Skip to content

Commit 9779d4e

Browse files
committed
Calculate incomplete integrals to absolute accuracy
1 parent 68cc0c1 commit 9779d4e

1 file changed

Lines changed: 157 additions & 73 deletions

File tree

src/arc.rs

Lines changed: 157 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ use crate::{
88
Point, Rect, Shape, Vec2,
99
};
1010
use core::{
11-
f64::consts::{FRAC_PI_2, PI},
11+
f64::{
12+
self,
13+
consts::{FRAC_PI_2, PI},
14+
},
1215
iter,
1316
ops::{Mul, Range},
1417
};
@@ -205,11 +208,7 @@ impl ParamCurve for Arc {
205208
}
206209

207210
impl ParamCurveArclen for Arc {
208-
fn arclen(&self, _accuracy: f64) -> f64 {
209-
// TODO: wire up accuracy. The Carlson numerical approximation provides a bound on the relative
210-
// error
211-
let relative_error = 1e-20;
212-
211+
fn arclen(&self, accuracy: f64) -> f64 {
213212
// Normalize ellipse to have radius y >= radius x, required for the parameter assumptions
214213
// of `incomplete_elliptic_integral_second_kind`.
215214
let (radii, mut start_angle) = if self.radii.y >= self.radii.x {
@@ -248,23 +247,43 @@ impl ParamCurveArclen for Arc {
248247
// that range.
249248
let mut arclen = 0.;
250249

250+
// The available accuracy (tolerance) is distributed over the calculation of the two
251+
// incomplete and one complete elliptic integrals.
252+
let accuracy_per_incomplete_integral = 1. / 3. * accuracy / radii.y;
251253
if start_angle >= PI / 2. {
252-
arclen += incomplete_elliptic_integral_second_kind(relative_error, PI - start_angle, m);
254+
arclen += incomplete_elliptic_integral_second_kind(
255+
accuracy_per_incomplete_integral,
256+
PI - start_angle,
257+
m,
258+
);
253259
quarter_turns -= 1.;
254260
} else {
255-
arclen -= incomplete_elliptic_integral_second_kind(relative_error, start_angle, m);
261+
arclen -= incomplete_elliptic_integral_second_kind(
262+
accuracy_per_incomplete_integral,
263+
start_angle,
264+
m,
265+
);
256266
}
257267

258268
if end_angle >= PI / 2. {
259-
arclen -= incomplete_elliptic_integral_second_kind(relative_error, PI - end_angle, m);
269+
arclen -= incomplete_elliptic_integral_second_kind(
270+
accuracy_per_incomplete_integral,
271+
PI - end_angle,
272+
m,
273+
);
260274
quarter_turns += 1.;
261275
} else {
262-
arclen += incomplete_elliptic_integral_second_kind(relative_error, end_angle, m);
276+
arclen += incomplete_elliptic_integral_second_kind(
277+
accuracy_per_incomplete_integral,
278+
end_angle,
279+
m,
280+
);
263281
}
264282
arclen *= radii.y;
265283

266-
// Note: this uses the complete elliptic integral, which can be special-cased.
267-
arclen += 0.25 * quarter_turns * complete_elliptic_perimeter(self.radii, relative_error);
284+
arclen += 1. / 4.
285+
* quarter_turns
286+
* complete_elliptic_perimeter(radii, 1. / 4. / 3. * accuracy * quarter_turns.max(1.));
268287

269288
arclen
270289
}
@@ -326,22 +345,49 @@ impl Mul<Arc> for Affine {
326345
/// elliptic integrals" (Carlson, Bille C.): <https://arxiv.org/abs/math/9409227v1>
327346
///
328347
/// RF = 1/2 ∫ 1 / ( sqrt(t+x) sqrt(t+y) sqrt(t+z) ) dt from 0 to inf
329-
fn carlson_rf(relative_error: f64, x: f64, y: f64, z: f64) -> f64 {
348+
fn carlson_rf(accuracy: f64, x: f64, y: f64, z: f64) -> f64 {
330349
// At most one of (x, y, z) may be 0.
331350
debug_assert!((x == 0.) as u8 + (y == 0.) as u8 + (z == 0.) as u8 <= 1);
332351

352+
// This mostly follows "Numerical computation of real or complex elliptic integrals", but using
353+
// an absolute upper error bound rather than a relative one.
354+
//
355+
// From "Numerical computation of real or complex elliptic integrals" we have
356+
//
357+
// X_n = (a_0 - x_0) / (4^n a_n)
358+
// (and the same for variables (Y,y), (Z,z)).
359+
//
360+
// From "Computing Elliptic Integrals by Duplication" we have an upper error bound of
361+
//
362+
// |err_n| < a_n^(-1/2) epsilon_n^6 / (4 (1 - epsilon_n))
363+
// with epsilon_n = max(X_n, Y_n, Z_n)
364+
// = max(a_0 - x_0, a_0 - y_0, a_0 - z_0) / (4^n a_n).
365+
//
366+
// Define e_0 = max(a_0 - x_0, a_0 - y_0, a_0 - z_0). Rewrite for ease of computation,
367+
//
368+
// |err_n| < a_n^(-1/2) epsilon_n^6 / (4 (1 - epsilon_n))
369+
// = a_n^(-1/2) e_0^6 / (4^n a_n)^6 / (4 (1 - epsilon_n))
370+
// -> |err_n| a_n^(1/2) (4^n a_n)^6 / e_0^6 < 1 / (4 (1 - epsilon_n))
371+
// -> |err_n| a_n^(1/2) a_n^6 4^(6n + 1) / e_0^6 < 1 / (1 - epsilon_n)
372+
// -> |err_n| a_n^(1/2) a_n^6 4^(6n + 1) / e_0^6 (1 - epsilon_n) < 1.
373+
//
374+
// To reach an error upper bound of `accuracy`, iterate until
375+
// 1 <= accuracy * a_n^(1/2) a_n^6 4^(6n + 1) / e_0^6 (1 - epsilon_n).
376+
333377
let mut x = x;
334378
let mut y = y;
335379
let mut z = z;
336380

337-
let a0 = (x + y + z) / 3.;
338-
let mut q = (3. * relative_error).powf(-1. / 6.)
339-
* (a0 - x).abs().max((a0 - y).abs()).max((a0 - z).abs());
381+
let mut a = (x + y + z) / 3.;
340382

341-
let mut a = a0;
342-
let mut m = 0;
383+
// These are partial terms of the inequality derived above. The multiply by (powers of) 4 are
384+
// performed per iteration for computational efficiency.
385+
let mut e = a - x.min(y).min(z);
386+
let mut r = accuracy * 4. * e.powi(-6);
387+
388+
// let mut q = 1. / (3. * f64::EPSILON).cbrt().sqrt() * (a - x.min(y).min(z));
343389
loop {
344-
if q <= a.abs() {
390+
if 1. <= r * a.powi(6) * a.sqrt() * (1. - e / a) {
345391
break;
346392
}
347393

@@ -351,81 +397,114 @@ fn carlson_rf(relative_error: f64, x: f64, y: f64, z: f64) -> f64 {
351397
y = (y + lambda) / 4.;
352398
z = (z + lambda) / 4.;
353399

354-
q /= 4.;
355-
m += 1;
400+
r *= 4f64.powi(6);
401+
e /= 4.;
356402
}
357403

358-
let x = (a0 - x) / 4f64.powi(m) * a;
359-
let y = (a0 - y) / 4f64.powi(m) * a;
404+
let x = 1. - x / a;
405+
let y = 1. - y / a;
360406
let z = -x - y;
361407

362408
let e2 = x * y - z.powi(2);
363409
let e3 = x * y * z;
364410

365-
1. / a.sqrt()
366-
* (1. - 1. / 10. * e2 + 1. / 14. * e3 + 1. / 24. * e2.powi(2) - 3. / 44. * e2 * e3)
411+
(1. + (-1. / 10. * e2 + 1. / 14. * e3 + 1. / 24. * e2.powi(2) - 3. / 44. * e2 * e3)) / a.sqrt()
367412
}
368413

369414
/// Approximation of the Carlson RD function as defined in "Numerical computation of real or
370415
/// complex elliptic integrals" (Carlson, Bille C.): <https://arxiv.org/abs/math/9409227v1>
371416
///
372417
/// RD = 3/2 ∫ 1 / ( sqrt(t+x) sqrt(t+y) (t+z)^(3/2) ) dt from 0 to inf
373-
fn carlson_rd(relative_error: f64, x: f64, y: f64, z: f64) -> f64 {
418+
fn carlson_rd(accuracy: f64, x: f64, y: f64, z: f64) -> f64 {
374419
// At most one of (x, y) may be 0, z must be nonzero.
375420
debug_assert!(z != 0.);
376421
debug_assert!(x != 0. || y != 0.);
377422

423+
// As above for RF, find the absolute upper error bound rather than a relative one, the
424+
// derivation of which is along the same lines.
425+
//
426+
// Again,
427+
//
428+
// X_n = (a_0 - x_0) / (4^n a_n)
429+
// (and the same for variables (Y,y), (Z,z)).
430+
//
431+
// From "Computing Elliptic Integrals by Duplication" we have
432+
//
433+
// |err_n| < 4^-n a_n^(-3/2) 3 epsilon_n^6 / (1 - epsilon_n)^(3/2)
434+
// with epsilon_n = max(X_n, Y_n, Z_n)
435+
// = max(a_0 - x_0, a_0 - y_0, a_0 - z_0) / (4^n a_n).
436+
//
437+
// Define e_0 = max(a_0 - x_0, a_0 - y_0, a_0 - z_0). Rewriting for ease of computation,
438+
//
439+
// |err_n| < 4^-n a_n^(-3/2) 3 epsilon_n^6 / (1 - epsilon_n)^(3/2)
440+
// = 4^-n a_n^(-3/2) 3 e_0^6 / 4^(6n) a_n^6 / (1 - epsilon_n)^(3/2)
441+
// -> |err_n| 4^(7n) a_n^(3/2) a_n^6 / (3 e_0^6) < 1 / (1 - epsilon_n)^(3/2)
442+
// -> |err_n| 4^(7n) a_n^(3/2) a_n^6 (1/3) / e_0^6 < (1 / 1 - epsilon_n)^(3/2),
443+
// raise to the power 2/3,
444+
// -> |err_n|^(2/3) 4^(14/3 n) a_n a_n^4 (1/3)^(2/3) / e_0^4 < 1 / (1 - epsilon_n)
445+
// -> |err_n|^(2/3) 4^(14/3 n) a_n^5 (1/3)^(2/3) / e_0^4 (1 - epsilon_n) < 1
446+
//
447+
// That means, to reach an error upper bound of `accuracy`, iterate until
448+
// 1 <= accuracy^(2/3) 4^(14/3 n) a_n^5 (1/3)^(2/3) / e_0^4 (1 - epsilon)
449+
378450
let mut x = x;
379451
let mut y = y;
380452
let mut z = z;
381453

382454
let a0 = (x + y + 3. * z) / 5.;
383-
let mut q = (relative_error / 4.).powf(-1. / 6.)
384-
* (a0 - x).abs().max((a0 - y).abs()).max((a0 - z).abs());
455+
let mut a = a0;
385456

386457
let mut sum = 0.;
387-
let mut a = a0;
388-
let mut m = 0;
458+
let mut mul = 1.;
459+
460+
// These are partial terms of the inequality derived above. The multiply by (powers of) 4 are
461+
// performed per iteration for computational efficiency.
462+
let mut e = a - x.min(y).min(z);
463+
let mut r = (accuracy / 3.).powf(2. / 3.) * e.powi(-4);
464+
389465
loop {
390-
if q <= a.abs() {
466+
if 1. <= r * a.powi(5) * (1. - e / a) {
391467
break;
392468
}
393469

394470
let lambda = (x * y).sqrt() + (x * z).sqrt() + (y * z).sqrt();
395-
sum += 4f64.powi(-m) / (z.sqrt() * (z + lambda));
471+
sum += mul / (z.sqrt() * (z + lambda));
396472
a = (a + lambda) / 4.;
397473
x = (x + lambda) / 4.;
398474
y = (y + lambda) / 4.;
399475
z = (z + lambda) / 4.;
400476

401-
q /= 4.;
402-
m += 1;
477+
r *= 4f64.powf(14. / 3.);
478+
e /= 4.;
479+
mul /= 4.;
403480
}
404481

405-
let x = (a0 - x) / (4f64.powi(4) * a);
406-
let y = (a0 - y) / (4f64.powi(4) * a);
407-
let z = -(x + y) / 3.;
482+
let x = 1. - x / a;
483+
let y = 1. - y / a;
484+
let z = (-x - y) / 3.;
408485

409486
let e2 = x * y - 6. * z.powi(2);
410487
let e3 = (3. * x * y - 8. * z.powi(2)) * z;
411-
let e4 = 3. * x * y - z.powi(2) * z.powi(2);
488+
let e4 = 3. * (x * y - z.powi(2)) * z.powi(2);
412489
let e5 = x * y * z.powi(3);
413490

414-
4f64.powi(-m) * 1. / (a * a.sqrt())
415-
* (1. - 3. / 14. * e2 + 1. / 6. * e3 + 9. / 88. * e2.powi(2)
416-
- 3. / 22. * e4
417-
- 9. / 52. * e2 * e3
418-
+ 3. / 26. * e5)
491+
(1. - 3. / 14. * e2 + 1. / 6. * e3 + 9. / 88. * e2.powi(2) - 3. / 22. * e4 - 9. / 52. * e2 * e3
492+
+ 3. / 26. * e5)
493+
* mul
494+
/ (a * a.sqrt())
419495
+ 3. * sum
420496
}
421497

422498
/// Numerically approximate the incomplete elliptic integral of the second kind from 0 to `phi`
423499
/// parameterized by `m = k^2` in Legendre's trigonometric form.
424500
///
501+
/// The absolute error between the calculated integral and the true integral is bounded by
502+
/// `accuracy` (modulo floating point rounding errors).
503+
///
425504
/// Assumes:
426505
/// 0 <= phi <= pi / 2
427506
/// and 0 <= m sin^2(phi) <= 1
428-
fn incomplete_elliptic_integral_second_kind(relative_error: f64, phi: f64, m: f64) -> f64 {
507+
fn incomplete_elliptic_integral_second_kind(accuracy: f64, phi: f64, m: f64) -> f64 {
429508
// Approximate the incomplete elliptic integral through Carlson symmetric forms:
430509
// https://en.wikipedia.org/w/index.php?title=Carlson_symmetric_form&oldid=1223277638#Incomplete_elliptic_integrals
431510

@@ -442,8 +521,25 @@ fn incomplete_elliptic_integral_second_kind(relative_error: f64, phi: f64, m: f6
442521
// note: this actually allows calculating from -1/2 pi <= phi <= 1/2 pi, but there are some
443522
// alternative translations from the Legendre form that are potentially better, that do
444523
// restrict the domain to 0 <= phi <= 1/2 pi.
445-
sin * carlson_rf(relative_error, cos2, 1. - m * sin2, 1.)
446-
- 1. / 3. * m * sin3 * carlson_rd(relative_error, cos2, 1. - m * sin2, 1.)
524+
let term1 = if sin == 0. {
525+
0.
526+
} else {
527+
sin * carlson_rf(
528+
accuracy / (2. * sin),
529+
// 1e-30,
530+
cos2,
531+
1. - m * sin2,
532+
1.,
533+
)
534+
};
535+
536+
let term2 = if sin == 0. || m == 0. {
537+
0.
538+
} else {
539+
1. / 3. * m * sin3 * carlson_rd(accuracy * 3. / 2. / (m * sin3), cos2, 1. - m * sin2, 1.)
540+
};
541+
542+
term1 - term2
447543
}
448544

449545
#[cfg(test)]
@@ -468,10 +564,6 @@ mod tests {
468564

469565
#[test]
470566
fn length() {
471-
// TODO: when arclen actually uses specified accuracy, update EPSILON and the accuracy
472-
// params
473-
const EPSILON: f64 = 1e-6;
474-
475567
// Circular checks:
476568
for (start_angle, sweep_angle, length) in [
477569
(0., 1., 1.),
@@ -482,37 +574,35 @@ mod tests {
482574
(2.5, 10., 10.),
483575
] {
484576
let a = Arc::new((0., 0.), (1., 1.), start_angle, sweep_angle, 0.);
485-
let arc_length = a.arclen(0.000_1);
577+
let arc_length = a.arclen(1e-7);
486578
assert!(
487-
(arc_length - length).abs() <= EPSILON,
579+
(arc_length - length).abs() <= 1e-6,
488580
"Got arc length {arc_length}, expected {length} for circular arc {a:?}"
489581
);
490582
}
491583

492584
let a = Arc::new((0., 0.), (1., 1.), 0., PI * 4., 0.);
493-
assert!((a.arclen(0.000_1) - PI * 4.).abs() <= EPSILON);
585+
assert!((a.arclen(1e-13) - PI * 4.).abs() <= 1e-12);
494586

495587
let a = Arc::new((0., 0.), (2.23, 3.05), 0., 0.2, 0.);
496-
assert!((a.arclen(0.000_1) - 0.60811714277).abs() <= EPSILON);
588+
assert!((a.arclen(1e-13) - 0.608_117_142_773_153_8).abs() <= 1e-12);
497589

498590
let a = Arc::new((0., 0.), (3.05, 2.23), 0., 0.2, 0.);
499-
assert!((a.arclen(0.000_1) - 0.448555).abs() <= EPSILON);
591+
assert!((a.arclen(1e-13) - 0.448_554_961_296_305_9).abs() <= 1e-12);
500592
}
501593

502594
#[test]
503595
fn length_compare_with_bez_length() {
504-
const EPSILON: f64 = 1e-3;
505-
506596
for radii in [(1., 1.), (0.5, 1.), (2., 1.)] {
507597
for start_angle in [0., 0.5, 1., 2., PI, -1.] {
508598
for sweep_angle in [0., 0.5, 1., 2., PI, -1.] {
509599
let a = Arc::new((0., 0.), radii, start_angle, sweep_angle, 0.);
510600

511-
let arc_length = a.arclen(0.000_1);
512-
let bez_length = a.path_segments(0.000_1).perimeter(0.000_1);
601+
let arc_length = a.arclen(1e-8);
602+
let bez_length = a.path_segments(1e-8).perimeter(1e-8);
513603

514604
assert!(
515-
(arc_length - bez_length).abs() < EPSILON,
605+
(arc_length - bez_length).abs() < 1e-7,
516606
"Numerically approximated arc length ({arc_length}) does not match bezier segment perimeter length ({bez_length}) for arc {a:?}"
517607
);
518608
}
@@ -522,33 +612,27 @@ mod tests {
522612

523613
#[test]
524614
fn carlson_numerical_checks() {
525-
// TODO: relative bound on error doesn't seem to be quite correct yet, use a large epsilon
526-
// for now
527-
const EPSILON: f64 = 1e-6;
528-
529615
// Numerical checks from section 3 of "Numerical computation of real or complex elliptic
530616
// integrals" (Carlson, Bille C.): https://arxiv.org/abs/math/9409227v1 (real-valued calls)
531-
assert!((carlson_rf(1e-20, 1., 2., 0.) - 1.311_028_777_146_1).abs() <= EPSILON);
532-
assert!((carlson_rf(1e-20, 2., 3., 4.) - 0.584_082_841_677_15).abs() <= EPSILON);
617+
assert!((carlson_rf(1e-13, 1., 2., 0.) - 1.311_028_777_146_1).abs() <= 1e-12);
618+
assert!((carlson_rf(1e-13, 2., 3., 4.) - 0.584_082_841_677_15).abs() <= 1e-12);
533619

534-
assert!((carlson_rd(1e-20, 0., 2., 1.) - 1.797_210_352_103_4).abs() <= EPSILON);
535-
assert!((carlson_rd(1e-20, 2., 3., 4.) - 0.165_105_272_942_61).abs() <= EPSILON);
620+
assert!((carlson_rd(1e-13, 0., 2., 1.) - 1.797_210_352_103_4).abs() <= 1e-12);
621+
assert!((carlson_rd(1e-13, 2., 3., 4.) - 0.165_105_272_942_61).abs() <= 1e-12);
536622
}
537623

538624
#[test]
539625
fn elliptic_e_numerical_checks() {
540-
const EPSILON: f64 = 1e-6;
541-
542626
for (phi, m, elliptic_e) in [
543627
(0.0, 0.0, 0.0),
544628
(0.5, 0.0, 0.5),
545629
(1.0, 0.0, 1.0),
546630
(0.0, 1.0, 0.0),
547-
(1.0, 1.0, 0.84147098),
631+
(1.0, 1.0, 0.841_470_984_807_896_5),
548632
] {
549-
let elliptic_e_approx = incomplete_elliptic_integral_second_kind(1e-20, phi, m);
633+
let elliptic_e_approx = incomplete_elliptic_integral_second_kind(1e-13, phi, m);
550634
assert!(
551-
(elliptic_e_approx - elliptic_e).abs() < EPSILON,
635+
(elliptic_e_approx - elliptic_e).abs() < 1e-12,
552636
"Approximated elliptic e {elliptic_e_approx} does not match known value {elliptic_e} for E({phi}|{m})"
553637
);
554638
}

0 commit comments

Comments
 (0)