use core::mem::swap;
use crate::{
carlson::{elliprc_unchecked, elliprd_unchecked, elliprf_unchecked},
crate_util::{case, check, declare, let_mut},
polyeval, StrErr,
};
use num_traits::Float;
#[numeric_literals::replace_float_literals(T::from(literal).unwrap())]
pub fn elliprj<T: Float>(x: T, y: T, z: T, p: T) -> Result<T, StrErr> {
check!(@nan, elliprj, [x, y, z, p]);
check!(@zero, elliprj, [p]);
check!(@neg, elliprj, "x, y, and z must be non-negative.", [x, y, z]);
check!(@multi_zero, elliprj, [x, y, z]);
case!(@any [x, y, z, p] == inf!(), T::zero());
let ans = elliprj_unchecked(x, y, z, p);
if ans.is_finite() {
return Ok(ans);
}
Err("elliprj: Failed to converge.")
}
#[inline]
#[numeric_literals::replace_float_literals(T::from(literal).unwrap())]
fn elliprc1p<T: Float>(y: T) -> T {
if y.abs() <= 0.01 {
let coeffs = [
1.0,
-1.0 / 3.0,
1.0 / 5.0,
-1.0 / 7.0,
1.0 / 9.0,
-1.0 / 11.0,
1.0 / 13.0,
-1.0 / 15.0,
1.0 / 17.0,
];
return polyeval(y, &coeffs);
}
if y > 0.0 {
y.sqrt().atan() / y.sqrt()
} else if y == 0.0 {
1.0
} else if y > -0.5 {
let arg = (-y).sqrt();
(arg.ln_1p() - (-arg).ln_1p()) / (2.0 * (-y).sqrt())
} else if y < -1.0 {
(1.0 / -y).sqrt() * elliprc_unchecked(-y, -1.0 - y)
} else {
((1.0 + (-y).sqrt()) / (1.0 + y).sqrt()).ln() / (-y).sqrt()
}
}
#[numeric_literals::replace_float_literals(T::from(literal).unwrap())]
fn elliprj_pv_near_zero<T: Float>(x: T, y: T, z: T, q: T, epsilon: T) -> Option<T> {
let radius = q * epsilon.sqrt();
if (x - 2.0 * q)
.abs()
.max((y - 2.0 * q).abs())
.max((z - 3.0 * q).abs())
> radius
{
return None;
}
let dx = (-2.0).mul_add(q, x);
let dy = (-2.0).mul_add(q, y);
let dz = (-3.0).mul_add(q, z);
if dx.abs().max(dy.abs()).max(dz.abs()) <= radius / 4.0 {
let raw_sum = dx + dy;
let a = 0.05188661383050757;
let b = 0.030423254727293472;
let bz = b * dz;
let linear = a.mul_add(raw_sum, bz);
let linear_error =
b.mul_add(dz, -bz) + 2.57998712160197e-18 * raw_sum - 1.0674999957272549e-18 * dz;
let sum = raw_sum / q;
let product = (dx / q) * (dy / q);
let dz = dz / q;
let quadratic = -0.024380373980361945 * sum * sum + 0.03250716530714926 * product
- 0.010731679551607051 * sum * dz
- 0.010479605394661824 * dz * dz;
let cubic = 0.0101259759655333 * sum * sum * sum - 0.02430234231727992 * sum * product
+ 0.004141427326475685 * sum * sum * dz
- 0.005521903101967579 * product * dz
+ 0.0028089569663743764 * sum * dz * dz
+ 0.0031555894162704945 * dz * dz * dz;
return Some(((linear + linear_error) / q + (quadratic + cubic)) / q / q.sqrt());
}
None
}
#[inline]
#[numeric_literals::replace_float_literals(T::from(literal).unwrap())]
pub fn elliprj_unchecked<T: Float>(x: T, y: T, z: T, p: T) -> T {
let_mut!(x, y, z);
if p <= 0.0 {
if x > y {
swap(&mut x, &mut y);
}
if y > z {
swap(&mut y, &mut z);
}
if x > y {
swap(&mut x, &mut y);
}
if p < 0.0 && T::epsilon() == T::from(f32::EPSILON).unwrap() {
if let (Some(x), Some(y), Some(z), Some(p)) =
(x.to_f64(), y.to_f64(), z.to_f64(), p.to_f64())
{
if [x, y, z, p].iter().all(|v| v.is_finite()) {
let value = elliprj_pv_near_zero(x, y, z, -p, f64::from(f32::EPSILON))
.unwrap_or_else(|| elliprj_unchecked(x, y, z, p));
if let Some(value) = T::from(value) {
return value;
}
}
}
}
let q = -p;
if let Some(value) = elliprj_pv_near_zero(x, y, z, q, T::epsilon()) {
return value;
}
}
let scale = x.abs().max(y.abs()).max(z.abs()).max(p.abs());
let limit = T::max_value().sqrt().sqrt().sqrt();
if scale.is_finite() && scale > 0.0 && (scale > limit || scale < 1.0 / limit)
&& [x, y, z, p].iter().all(|&v| v == 0.0 || v / scale != 0.0)
{
return elliprj_unchecked(x / scale, y / scale, z / scale, p / scale)
/ scale
/ scale.sqrt();
}
if p <= 0.0 {
let q = -p;
let z_plus_q = z + q;
let xy = x * y;
let p = (z * (x + y + q) - xy) / z_plus_q;
let pq = p * q;
let xy_plus_pq = xy + pq;
let xyz = xy * z;
let rj = elliprj_unchecked(x, y, z, p);
let rf = elliprf_unchecked(x, y, z);
let rc = elliprc_unchecked(xy_plus_pq, pq);
let mut value = (p - z) * rj;
value = value - 3.0 * rf;
value = value + 3.0 * (xyz / xy_plus_pq).sqrt() * rc;
return value / z_plus_q;
}
if x == y {
if x == z {
if x == p {
return 1.0 / (x * x.sqrt());
} else if p.max(x) / p.min(x) > 1.2 {
return (3.0 / (x - p)) * (elliprc_unchecked(x, p) - 1.0 / x.sqrt());
}
} else {
swap(&mut x, &mut z);
}
}
if y == z {
if y == p {
return elliprd_unchecked(x, y, y);
}
if p.max(y) / p.min(y) > 1.2 && x <= y.max(p) {
return 3.0 * (elliprc_unchecked(x, y) - elliprc_unchecked(x, p)) / (p - y);
}
}
if z == p {
return elliprd_unchecked(x, y, z);
}
declare!(mut [xn = x, yn = y, zn = z, pn = p]);
let mut an = (x + y + z + 2.0 * p) / 5.0;
let a0 = an;
let mut delta = (p - x) * (p - y) * (p - z);
let q = (epsilon!() / 5.0).powf(-1.0 / 8.0)
* (an - x)
.abs()
.max((an - y).abs())
.max((an - z).abs())
.max((an - p).abs());
let mut fmn = 1.0;
let mut rc_sum = 0.0;
let mut ans = nan!();
for _ in 0..N_MAX_ITERATION {
let rx = xn.sqrt();
let ry = yn.sqrt();
let rz = zn.sqrt();
let rp = pn.sqrt();
let dn = (rp + rx) * (rp + ry) * (rp + rz);
let dn2 = dn * dn;
let en = if dn2 >= T::min_positive_value() {
delta / dn2
} else {
((pn - xn) / (rp + rx) / (rp + rx))
* ((pn - yn) / (rp + ry) / (rp + ry))
* ((pn - zn) / (rp + rz) / (rp + rz))
};
if en < -0.5 && en > -1.5 {
let b = 2.0 * rp * (pn + rx * (ry + rz) + ry * rz) / dn;
rc_sum = rc_sum + fmn / dn * elliprc_unchecked(1.0, b);
} else {
rc_sum = rc_sum + fmn / dn * elliprc1p(en);
}
let lambda = rx * ry + rx * rz + ry * rz;
an = (an + lambda) / 4.0;
fmn = fmn / 4.0;
if fmn * q < an {
let x = fmn * (a0 - x) / an;
let y = fmn * (a0 - y) / an;
let z = fmn * (a0 - z) / an;
let p = (-x - y - z) / 2.0;
let xyz = x * y * z;
let p2 = p * p;
let p3 = p2 * p;
let e2 = x * y + x * z + y * z - 3.0 * p2;
let e3 = xyz + 2.0 * e2 * p + 4.0 * p3;
let e4 = (2.0 * xyz + e2 * p + 3.0 * p3) * p;
let e5 = xyz * p2;
let result = fmn
* an.powf(-1.5)
* (1.0 - 3.0 * e2 / 14.0 + e3 / 6.0 + 9.0 * e2 * e2 / 88.0
- 3.0 * e4 / 22.0
- 9.0 * e2 * e3 / 52.0
+ 3.0 * e5 / 26.0
- e2 * e2 * e2 / 16.0
+ 3.0 * e3 * e3 / 40.0
+ 3.0 * e2 * e4 / 20.0
+ 45.0 * e2 * e2 * e3 / 272.0
- 9.0 * (e3 * e4 + e2 * e5) / 68.0);
ans = result + 6.0 * rc_sum;
break;
}
xn = (xn + lambda) / 4.0;
yn = (yn + lambda) / 4.0;
zn = (zn + lambda) / 4.0;
pn = (pn + lambda) / 4.0;
delta = delta / 64.0;
}
ans
}
#[cfg(not(feature = "test_force_fail"))]
const N_MAX_ITERATION: usize = 100;
#[cfg(feature = "test_force_fail")]
const N_MAX_ITERATION: usize = 1;
#[cfg(not(feature = "test_force_fail"))]
#[cfg(test)]
mod tests {
use itertools::Itertools;
use super::*;
use crate::{assert_close, compare_test_data_boost, compare_test_data_wolfram};
fn __elliprj(inp: &[&f64]) -> f64 {
elliprj(*inp[0], *inp[1], *inp[2], *inp[3]).unwrap()
}
fn _elliprj(inp: &[f64]) -> f64 {
let res = elliprj(inp[0], inp[1], inp[2], inp[3]).unwrap();
let (p, sym_params) = inp.split_last().unwrap();
sym_params
.iter()
.permutations(sym_params.len())
.skip(1)
.for_each(|mut perm| {
perm.push(p);
assert_close!(res, __elliprj(&perm), 3e-14);
});
res
}
#[test]
fn test_elliprj_rounding_edges() {
for (x, expected) in [
(2.0, 0.5228004174989865),
(2.0 + 2.0 * f64::EPSILON, 0.5228004174989864),
(1e4, 0.010391048517489909),
(1e12, 1.0397207708199448e-6),
(1e100, 1.039720770839918e-50),
(1e250, 1.039720770839918e-125),
] {
for scale in [2.0_f64.powi(-100), 1.0, 2.0_f64.powi(100)] {
let input = [x * scale, scale, scale];
for perm in input.iter().permutations(3) {
let actual = elliprj(*perm[0], *perm[1], *perm[2], 2.0 * scale).unwrap();
crate::util::assert_close(
actual * scale * scale.sqrt(),
expected,
8.0 * f64::EPSILON,
);
}
}
}
}
#[test]
fn test_elliprj_rounding_edges_f32() {
for (x, expected) in [(1e12_f32, 1.0397208e-6_f32), (1e30, 1.0397208e-15)] {
for input in [[x, 1.0, 1.0], [1.0, x, 1.0], [1.0, 1.0, x]] {
let actual = elliprj(input[0], input[1], input[2], 2.0).unwrap();
crate::util::assert_close(actual, expected, 3.0 * f32::EPSILON);
}
}
}
#[test]
fn test_elliprj_pv_near_zero() {
for (input, expected) in [
([2.0000000000000004, 2.0, 3.0, -1.0], 2.3042285337785435e-17),
(
[1.9999999999999996, 2.0, 3.0, -1.0],
-2.3042285337785444e-17,
),
(
[2.0000000000000004, 1.9999999999999996, 3.0, -1.0],
-6.410907962591709e-33,
),
(
[2.0000000037252903, 1.9999999962747097, 3.0, -1.0],
-4.511275423661651e-19,
),
(
[2.0000000009313226, 2.0, 2.999999998411637, -1.0],
-4.053879912836731e-18,
),
(
[0.800000000372529, 0.8, 1.1999999993646548, -0.4],
-7.382637188177162e-18,
),
(
[
0.8000000000000002,
0.8000000000000005,
1.2000000000000008,
-0.4000000000000001,
],
3.54361305087397e-16,
),
(
[
0.8000000000000002,
1.2000000000000006,
0.8000000000000008,
-0.4000000000000001,
],
4.583843736444436e-16,
),
(
[
1.2000000000000002,
0.8000000000000005,
0.8000000000000008,
-0.4000000000000001,
],
4.956505791267302e-16,
),
] {
for scale in [2.0_f64.powi(-200), 1.0, 2.0_f64.powi(200)] {
for perm in input[..3].iter().permutations(3) {
let actual = elliprj(
*perm[0] * scale,
*perm[1] * scale,
*perm[2] * scale,
input[3] * scale,
)
.unwrap()
* scale
* scale.sqrt();
assert!(
(actual / expected - 1.0).abs() <= 8.0 * f64::EPSILON,
"input={input:?}, scale={scale}, actual={actual}, expected={expected}"
);
assert_eq!(actual.is_sign_negative(), expected.is_sign_negative());
}
}
}
for scale in [2.0_f64.powi(-200), 1.0, 2.0_f64.powi(200)] {
assert_eq!(
elliprj(2.0 * scale, 2.0 * scale, 3.0 * scale, -scale),
Ok(0.0)
);
}
}
#[test]
fn test_elliprj_pv_expansion_boundary() {
for (z, expected, tolerance) in [
(
3.0 + 2.0_f64.powi(-28),
1.1333545553778819e-10,
8.0 * f64::EPSILON * 1.1333545553778819e-10,
),
(3.0000000037252907, 1.1333546904842731e-10, f64::EPSILON),
] {
let actual = elliprj(2.0, 2.0, z, -1.0).unwrap();
assert!((actual - expected).abs() <= tolerance);
assert!(actual > 0.0);
}
}
#[test]
fn test_elliprj_pv_rounding_f32() {
for (input, expected) in [
(
[2.0_f32 + 2.0 * f32::EPSILON, 2.0, 3.0, -1.0],
1.2370731e-8_f32,
),
([2.0 - 2.0 * f32::EPSILON, 2.0, 3.0, -1.0], -1.2370734e-8),
(
[
2.0 + 2.0 * f32::EPSILON,
2.0 - 2.0 * f32::EPSILON,
3.0,
-1.0,
],
-1.8478185e-15,
),
([0.8000001, 0.80000025, 1.2000004, -0.4000001], 5.277546e-9),
([0.8000001, 1.2000003, 0.80000037, -0.4000001], 3.0562223e-8),
([1.2, 0.80000025, 0.80000037, -0.4000001], 5.0569344e-8),
] {
for scale in [2.0_f32.powi(-20), 1.0, 2.0_f32.powi(20)] {
for perm in input[..3].iter().permutations(3) {
let actual = elliprj(
*perm[0] * scale,
*perm[1] * scale,
*perm[2] * scale,
input[3] * scale,
)
.unwrap()
* scale
* scale.sqrt();
assert!(
(actual / expected - 1.0).abs() <= 2.0 * f32::EPSILON,
"input={input:?}, scale={scale}, actual={actual}, expected={expected}"
);
assert_eq!(actual.is_sign_negative(), expected.is_sign_negative());
}
}
}
for scale in [2.0_f32.powi(-70), 1.0, 2.0_f32.powi(70)] {
let actual = elliprj(scale, 2.0 * scale, 3.0 * scale, -4.0 * scale).unwrap()
* scale
* scale.sqrt();
crate::util::assert_close(actual, -0.2378677, 2.0 * f32::EPSILON);
assert_eq!(
elliprj(2.0 * scale, 2.0 * scale, 3.0 * scale, -scale),
Ok(0.0)
);
}
for case in ellip_dev_utils::parser::read_wolfram_data::<f32>(
"tests/data/f32/wolfram/elliprj_pv.csv",
)
.unwrap()
{
let actual = elliprj(
case.inputs[0],
case.inputs[1],
case.inputs[2],
case.inputs[3],
)
.unwrap();
assert_eq!(actual, case.expected, "input={:?}", case.inputs);
}
}
#[test]
fn test_elliprj() {
compare_test_data_boost!("elliprj_data.txt", _elliprj, 2.7e-14, atol: 5e-25);
compare_test_data_boost!("elliprj_e2.txt", _elliprj, 4.8e-14, atol: 5e-25);
compare_test_data_boost!("elliprj_e3.txt", _elliprj, 3.1e-15, atol: 5e-25);
compare_test_data_boost!("elliprj_e4.txt", _elliprj, 2.2e-16, atol: 5e-25);
compare_test_data_boost!("elliprj_zp.txt", _elliprj, 3.5e-15, atol: 5e-25);
}
#[test]
fn test_elliprj_wolfram() {
compare_test_data_wolfram!("elliprj_data.csv", elliprj, 4, 3.1e-15);
compare_test_data_wolfram!("elliprj_pv.csv", elliprj, 4, 5e-14, atol: f64::EPSILON);
}
#[test]
fn test_elliprj_special_cases() {
use std::f64::{INFINITY, NAN};
assert_eq!(
elliprj(-1.0, 1.0, 1.0, 1.0),
Err("elliprj: x, y, and z must be non-negative.")
);
assert_eq!(
elliprj(1.0, -1.0, 1.0, 1.0),
Err("elliprj: x, y, and z must be non-negative.")
);
assert_eq!(
elliprj(1.0, 1.0, -1.0, 1.0),
Err("elliprj: x, y, and z must be non-negative.")
);
assert_eq!(
elliprj(0.0, 0.0, 1.0, 1.0),
Err("elliprj: At most one argument can be zero.")
);
assert_eq!(
elliprj(0.0, 1.0, 0.0, 1.0),
Err("elliprj: At most one argument can be zero.")
);
assert_eq!(
elliprj(1.0, 0.0, 0.0, 1.0),
Err("elliprj: At most one argument can be zero.")
);
assert_eq!(
elliprj(1.0, 1.0, 1.0, 0.0),
Err("elliprj: p cannot be zero.")
);
assert_eq!(
elliprj(NAN, 1.0, 1.0, 1.0),
Err("elliprj: Arguments cannot be NAN.")
);
assert_eq!(
elliprj(1.0, NAN, 1.0, 1.0),
Err("elliprj: Arguments cannot be NAN.")
);
assert_eq!(
elliprj(1.0, 1.0, NAN, 1.0),
Err("elliprj: Arguments cannot be NAN.")
);
assert_eq!(
elliprj(1.0, 1.0, 1.0, NAN),
Err("elliprj: Arguments cannot be NAN.")
);
assert_eq!(elliprj(INFINITY, 1.0, 1.0, 1.0).unwrap(), 0.0);
assert_eq!(elliprj(1.0, INFINITY, 1.0, 1.0).unwrap(), 0.0);
assert_eq!(elliprj(1.0, 1.0, INFINITY, 1.0).unwrap(), 0.0);
assert_eq!(elliprj(1.0, 1.0, 1.0, INFINITY).unwrap(), 0.0);
}
#[test]
fn cover_deadcode() {
assert!(elliprc1p(-0.6).is_finite());
assert!(elliprc1p(-1.1).is_finite());
}
#[test]
fn test_elliprj_invalid_input_does_not_abort() {
if std::env::var("ELLIP_A05_RJ_CHILD").is_ok() {
assert!(elliprj(0.0, 0.0, 1.0, 0.0).is_err());
return;
}
let status = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"carlson::elliprj::tests::test_elliprj_invalid_input_does_not_abort",
])
.env("ELLIP_A05_RJ_CHILD", "1")
.status()
.unwrap();
assert!(
status.success(),
"invalid RJ input aborted or failed: {status}"
);
}
#[test]
fn test_elliprj_extreme_scales() {
let actual = elliprj(1e110, 2e110, 3e110, 4e110).unwrap();
let expected = 2.3984809974956775e-166;
assert!(actual.is_finite());
assert!((actual - expected).abs() <= 2e-15 * expected);
for scale in [1e-200_f64, 1e-100, 1e100, 1e200] {
let actual = elliprj(scale, 2.0 * scale, 3.0 * scale, 4.0 * scale).unwrap()
* scale
* scale.sqrt();
let expected = elliprj(1.0, 2.0, 3.0, 4.0).unwrap();
assert!((actual - expected).abs() <= 3e-15 * expected.abs());
}
}
#[test]
fn test_near_equal_arguments() {
for (p, expected) in [
(f64::from_bits(1.0f64.to_bits() + 1), 0.9999999999999999),
(1.000000000001, 0.9999999999993999),
(f64::from_bits(1.0f64.to_bits() - 1), 1.0),
] {
let actual = elliprj(1.0, 1.0, 1.0, p).unwrap();
crate::assert_close!(actual, expected, 8e-16);
if p > 1.0 {
assert!(actual <= 1.0);
}
}
crate::assert_close!(
elliprj(2.0, 2.0, 2.0, 2.000000000002).unwrap(),
0.35355339059306163,
1e-15
);
crate::assert_close!(
elliprj(1.0f32, 1.0, 1.0, f32::from_bits(1.0f32.to_bits() + 1)).unwrap() as f64,
1.0,
3e-7
);
}
}
#[cfg(feature = "test_force_fail")]
crate::test_force_unreachable! {
assert_eq!(elliprj(0.2, 0.5, 1e300, 1.0), Err("elliprj: Failed to converge."));
}