From 50a257868ea80c093c70e475b09583582f6df0ec Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Sat, 29 Aug 2020 12:00:13 -0700 Subject: [PATCH] Improve the precision of lerpDouble Reduces errors caused by the loss of floating point precision when the two extrema of the lerp differ significantly in magnitude. Previously, we used the calculation: a + (b - a) * t When the difference in magnitude between `a` and `b` exceeds the precision representable by double-precision floating point math, `b - a` results in the larger-magnitude value of `a` or `b`. The error between the value produced and the correct value is then scaled by t. A simple example of the impact can be seen when `a` is significantly larger in magnitude than `b`. In that case, `b - a` results in `a` and when `t` is 1.0, the resulting value is `a - (a) * 1.0 == 0`. The patch transforms the computation to the mathematically-equivalent expression: a * (1.0 - t) + b * t By scaling each value independently, the behaviour is more accurate. From the point of view of performance, this adds an extra multiplication, but multiplication is relatively cheap and the behaviour is significantly better. This patch also adds a `precisionErrorTolerance` constant to test_utils.dart and migrates existing tests to use `closeTo()` for testing. The tests themselves *do* currently use values that have an exact floating-point representation, but we should allow for flexibility in future implementation changes. --- lib/ui/lerp.dart | 6 +++--- testing/dart/lerp_test.dart | 41 ++++++++++++++++++++++--------------- testing/dart/test_util.dart | 7 +++++++ 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/lib/ui/lerp.dart b/lib/ui/lerp.dart index c4841e2ae37b6..2357433ba33d8 100644 --- a/lib/ui/lerp.dart +++ b/lib/ui/lerp.dart @@ -20,21 +20,21 @@ double? lerpDouble(num? a, num? b, double t) { assert(a.isFinite, 'Cannot interpolate between finite and non-finite values'); assert(b.isFinite, 'Cannot interpolate between finite and non-finite values'); assert(t.isFinite, 't must be finite when interpolating between values'); - return a + (b - a) * t as double; + return a * (1.0 - t) + b * t as double; } /// Linearly interpolate between two doubles. /// /// Same as [lerpDouble] but specialized for non-null `double` type. double _lerpDouble(double a, double b, double t) { - return a + (b - a) * t; + return a * (1.0 - t) + b * t; } /// Linearly interpolate between two integers. /// /// Same as [lerpDouble] but specialized for non-null `int` type. double _lerpInt(int a, int b, double t) { - return a + (b - a) * t; + return a * (1.0 - t) + b * t; } /// Same as [num.clamp] but specialized for non-null [int]. diff --git a/testing/dart/lerp_test.dart b/testing/dart/lerp_test.dart index 4395e974747f7..d9e43092c95e7 100644 --- a/testing/dart/lerp_test.dart +++ b/testing/dart/lerp_test.dart @@ -20,19 +20,19 @@ void main() { }); test('lerpDouble should treat a null input as 0 if the other input is non-null', () { - expect(lerpDouble(null, 10.0, 0.25), 2.5); - expect(lerpDouble(10.0, null, 0.25), 7.5); + expect(lerpDouble(null, 10.0, 0.25), closeTo(2.5, precisionErrorTolerance)); + expect(lerpDouble(10.0, null, 0.25), closeTo(7.5, precisionErrorTolerance)); - expect(lerpDouble(null, 10, 0.25), 2.5); - expect(lerpDouble(10, null, 0.25), 7.5); + expect(lerpDouble(null, 10, 0.25), closeTo(2.5, precisionErrorTolerance)); + expect(lerpDouble(10, null, 0.25), closeTo(7.5, precisionErrorTolerance)); }); test('lerpDouble should handle interpolation values < 0.0', () { - expect(lerpDouble(0.0, 10.0, -5.0), -50.0); - expect(lerpDouble(10.0, 0.0, -5.0), 60.0); + expect(lerpDouble(0.0, 10.0, -5.0), closeTo(-50.0, precisionErrorTolerance)); + expect(lerpDouble(10.0, 0.0, -5.0), closeTo(60.0, precisionErrorTolerance)); - expect(lerpDouble(0, 10, -5), -50); - expect(lerpDouble(10, 0, -5), 60); + expect(lerpDouble(0, 10, -5), closeTo(-50, precisionErrorTolerance)); + expect(lerpDouble(10, 0, -5), closeTo(60, precisionErrorTolerance)); }); test('lerpDouble should return the start value at 0.0', () { @@ -44,11 +44,17 @@ void main() { }); test('lerpDouble should interpolate between two values', () { - expect(lerpDouble(0.0, 10.0, 0.25), 2.5); - expect(lerpDouble(10.0, 0.0, 0.25), 7.5); + expect(lerpDouble(0.0, 10.0, 0.25), closeTo(2.5, precisionErrorTolerance)); + expect(lerpDouble(10.0, 0.0, 0.25), closeTo(7.5, precisionErrorTolerance)); - expect(lerpDouble(0, 10, 0.25), 2.5); - expect(lerpDouble(10, 0, 0.25), 7.5); + expect(lerpDouble(0, 10, 0.25), closeTo(2.5, precisionErrorTolerance)); + expect(lerpDouble(10, 0, 0.25), closeTo(7.5, precisionErrorTolerance)); + + // Exact answer: 20.0 - 1.0e-29 + expect(lerpDouble(10.0, 1.0e30, 1.0e-29), closeTo(20.0, precisionErrorTolerance)); + + // Exact answer: 5.0 + 5.0e29 + expect(lerpDouble(10.0, 1.0e30, 0.5), closeTo(5.0e29, precisionErrorTolerance)); }); test('lerpDouble should return the end value at 1.0', () { @@ -57,14 +63,17 @@ void main() { expect(lerpDouble(0, 10, 5), 50); expect(lerpDouble(10, 0, 5), -40); + + expect(lerpDouble(1.0e30, 10.0, 1.0), 10.0); + expect(lerpDouble(10.0, 1.0e30, 0.0), 10.0); }); test('lerpDouble should handle interpolation values > 1.0', () { - expect(lerpDouble(0.0, 10.0, 5.0), 50.0); - expect(lerpDouble(10.0, 0.0, 5.0), -40.0); + expect(lerpDouble(0.0, 10.0, 5.0), closeTo(50.0, precisionErrorTolerance)); + expect(lerpDouble(10.0, 0.0, 5.0), closeTo(-40.0, precisionErrorTolerance)); - expect(lerpDouble(0, 10, 5), 50); - expect(lerpDouble(10, 0, 5), -40); + expect(lerpDouble(0, 10, 5), closeTo(50, precisionErrorTolerance)); + expect(lerpDouble(10, 0, 5), closeTo(-40, precisionErrorTolerance)); }); test('lerpDouble should return input value in all cases if begin/end are equal', () { diff --git a/testing/dart/test_util.dart b/testing/dart/test_util.dart index e990b138e7f3e..9ef1f2d25f50d 100644 --- a/testing/dart/test_util.dart +++ b/testing/dart/test_util.dart @@ -6,6 +6,13 @@ import 'package:test/test.dart'; +/// The epsilon of tolerable double precision error. +/// +/// This is used in various places in the framework to allow for floating point +/// precision loss in calculations. Differences below this threshold are safe +/// to disregard. +const double precisionErrorTolerance = 1e-10; + /// Asserts that `callback` throws an [AssertionError]. /// /// When running in a VM in which assertions are enabled, asserts that the