Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@cbracken
Copy link
Member

@cbracken cbracken commented Aug 29, 2020

Description

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 result produced 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.

Tests

Added four tests that verify behaviour of values with large differences in magnitude:

  • at either endpoint
  • within the range

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.

Related patches

flutter/flutter#64908 migrates a few framework tests where we were using exact double equality checks rather than using precisionErrorTolerance (from the Foundation layer of the framework) or moreOrLessEquals to test values that have been scaled/lerped and is required to fix four framework test failures triggered by this patch due to mismatches in the ~15th decimal place.

@cbracken
Copy link
Member Author

cbracken commented Aug 29, 2020

This is currently rebased on top of #20874, #20880 and #20871. Only the most recent commit should be reviewed.

Those patches are now landed and this is rebased to tip-of-tree.

@cbracken cbracken requested a review from Hixie August 29, 2020 20:34
@cbracken cbracken changed the title Cleanup lerp double Cleanup lerpDouble Aug 29, 2020
@cbracken cbracken changed the title Cleanup lerpDouble Improve the precision of lerpDouble Aug 29, 2020
@cbracken cbracken requested a review from chinmaygarde August 29, 2020 21:01
@cbracken
Copy link
Member Author

cbracken commented Aug 30, 2020

In presubmit, this actually does cause four lerp-related tests in the framework to start failing.

An example is the HSVColor control test which is effectively equivalent to this check:

expect(lerpDouble(0.7, 0.3, 0.25), 0.6);

the result of which differs in the ~15th decimal place. This is likely due to the use of 0.7, 0.3, and 0.6, which can't be represented exactly in a double. One option would be to rewrite those four tests to use inputs and scale factors with exact double representations such that those checks are a bit more tolerant of minor implementation changes.

I think this change is still worth making given the big correctness wins in cases with significant differences in magnitude of the inputs, vs the very tiny differences in the few test cases relying on values that can't be exactly represented.

@cbracken
Copy link
Member Author

Alright, landed all the stacked dependent PRs. PTAL.

@cbracken
Copy link
Member Author

cbracken commented Aug 30, 2020

I've sent flutter/flutter#64908 to fix the four framework tests mentioned above to use expect(actual, closeTo(expected, precisionErrorTolerance)) as we do in other tests where we're checking floating point values that have been scaled or had other operations done to them.

I've also updated the lerpDouble tests in this patch to use closeTo() to match any scaled values other than where t == 0 or t == 1 where we expect exact values. The tests here use values that have an exact floating point representation and the current implementation is such that the values will remain exactly-representable but we should allow flexibility for future fixes.

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.
@cbracken
Copy link
Member Author

flutter/flutter#64908 has landed which fixes the four framework tests mentioned above.

/// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one doesn't need it

Copy link
Contributor

@Hixie Hixie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@cbracken cbracken merged commit 784e6d7 into flutter:master Aug 31, 2020
@cbracken cbracken deleted the cleanup-lerpDouble branch August 31, 2020 05:26
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Aug 31, 2020
fluttergithubbot pushed a commit to flutter/flutter that referenced this pull request Aug 31, 2020
mingwandroid pushed a commit to mingwandroid/flutter that referenced this pull request Sep 6, 2020
cbracken added a commit that referenced this pull request Sep 8, 2020
This updates the web_ui implementation of lerpDouble to match the
behaviour of the C++ engine implementation in dart:ui.

Specifically this covers the following changes:
* #20871: stricter handling of NaN and infinity
* #20879: Improve the precision of lerpDouble

lerpDouble: stricter handling of NaN and infinity (#20871)
----------------------------------------------------------

Previously, the behaviour of lerpDouble with respect to NaN and infinity
was relatively complex and difficult to reason about. This patch
simplifies the behaviour with respect to those conditions and adds
documentation and tests.

In general, if `a == b` or both values are null, infinite, or NaN, `a`
is returned. Otherwise we require `a` and `b` and `t` to be finite or
null and the result of the linear interpolation is returned.

Improve the precision of lerpDouble (#20879)
--------------------------------------------

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.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants