about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMara Bos <m-ou.se@m-ou.se>2021-06-17 23:40:57 +0200
committerGitHub <noreply@github.com>2021-06-17 23:40:57 +0200
commitfcac47896696899badc9a436d798d48158895ea3 (patch)
tree08302428c205c02b60d6b0d142a2127737f4480e
parente062e5d34ee80e0d5da403e8c5609f29e6a7dbcb (diff)
parent525d76026fe855f6a9de4604d9fee50d974994a3 (diff)
downloadrust-fcac47896696899badc9a436d798d48158895ea3.tar.gz
rust-fcac47896696899badc9a436d798d48158895ea3.zip
Rollup merge of #85925 - clarfonthey:lerp, r=m-ou-se
Linear interpolation

#71016 is a previous attempt at implementation that was closed by the author. I decided to reuse the feature request issue (#71015) as a tracking issue. A member of the rust-lang org will have to edit the original post to be formatted correctly as I am not the issue's original author.

The common name `lerp` is used because it is the term used by most code in a wide variety of contexts; it also happens to be the recently chosen name of the function that was added to C++20.

To ensure symmetry as a method, this breaks the usual ordering of the method from `lerp(a, b, t)` to `t.lerp(a, b)`. This makes the most sense to me personally, and there will definitely be discussion before stabilisation anyway.

Implementing lerp "correctly" is very dififcult even though it's a very common building-block used in all sorts of applications. A good prior reading is [this proposal](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0811r2.html#linear-interpolation) for the C++20 lerp which talks about the various guarantees, which I've simplified down to:

1. Exactness: `(0.0).lerp(start, end) == start` and `(1.0).lerp(start, end) == end`
2. Consistency: `anything.lerp(x, x) == x`
3. Monotonicity: once you go up don't go down

Fun story: the version provided in that proposal, from what I understand, isn't actually monotonic.

I messed around with a *lot* of different lerp implementations because I kind of got a bit obsessed and I ultimately landed on one that uses the fused `mul_add` instruction. Floating-point lerp lore is hard to come by, so, just trust me when I say that this ticks all the boxes. I'm only 90% certain that it's monotonic, but I'm sure that people who care deeply about this will be there to discuss before stabilisation.

The main reason for using `mul_add` is that, in general, it ticks more boxes with fewer branches to be "correct." Although it will be slower on architectures without the fused `mul_add`, that's becoming more and more rare and I have a feeling that most people who will find themselves needing `lerp` will also have an efficient `mul_add` instruction available.
-rw-r--r--library/std/src/f32.rs36
-rw-r--r--library/std/src/f32/tests.rs63
-rw-r--r--library/std/src/f64.rs36
-rw-r--r--library/std/src/f64/tests.rs55
-rw-r--r--library/std/src/lib.rs1
5 files changed, 191 insertions, 0 deletions
diff --git a/library/std/src/f32.rs b/library/std/src/f32.rs
index c16d27fa1f5..21bd79611a5 100644
--- a/library/std/src/f32.rs
+++ b/library/std/src/f32.rs
@@ -876,4 +876,40 @@ impl f32 {
     pub fn atanh(self) -> f32 {
         0.5 * ((2.0 * self) / (1.0 - self)).ln_1p()
     }
+
+    /// Linear interpolation between `start` and `end`.
+    ///
+    /// This enables linear interpolation between `start` and `end`, where start is represented by
+    /// `self == 0.0` and `end` is represented by `self == 1.0`. This is the basis of all
+    /// "transition", "easing", or "step" functions; if you change `self` from 0.0 to 1.0
+    /// at a given rate, the result will change from `start` to `end` at a similar rate.
+    ///
+    /// Values below 0.0 or above 1.0 are allowed, allowing you to extrapolate values outside the
+    /// range from `start` to `end`. This also is useful for transition functions which might
+    /// move slightly past the end or start for a desired effect. Mathematically, the values
+    /// returned are equivalent to `start + self * (end - start)`, although we make a few specific
+    /// guarantees that are useful specifically to linear interpolation.
+    ///
+    /// These guarantees are:
+    ///
+    /// * If `start` and `end` are [finite], the value at 0.0 is always `start` and the
+    ///   value at 1.0 is always `end`. (exactness)
+    /// * If `start` and `end` are [finite], the values will always move in the direction from
+    ///   `start` to `end` (monotonicity)
+    /// * If `self` is [finite] and `start == end`, the value at any point will always be
+    ///   `start == end`. (consistency)
+    ///
+    /// [finite]: #method.is_finite
+    #[must_use = "method returns a new number and does not mutate the original value"]
+    #[unstable(feature = "float_interpolation", issue = "86269")]
+    pub fn lerp(self, start: f32, end: f32) -> f32 {
+        // consistent
+        if start == end {
+            start
+
+        // exact/monotonic
+        } else {
+            self.mul_add(end, (-self).mul_add(start, start))
+        }
+    }
 }
diff --git a/library/std/src/f32/tests.rs b/library/std/src/f32/tests.rs
index 0d4b865f339..fe66a73afd6 100644
--- a/library/std/src/f32/tests.rs
+++ b/library/std/src/f32/tests.rs
@@ -757,3 +757,66 @@ fn test_total_cmp() {
     assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&f32::INFINITY));
     assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&s_nan()));
 }
+
+#[test]
+fn test_lerp_exact() {
+    // simple values
+    assert_eq!(f32::lerp(0.0, 2.0, 4.0), 2.0);
+    assert_eq!(f32::lerp(1.0, 2.0, 4.0), 4.0);
+
+    // boundary values
+    assert_eq!(f32::lerp(0.0, f32::MIN, f32::MAX), f32::MIN);
+    assert_eq!(f32::lerp(1.0, f32::MIN, f32::MAX), f32::MAX);
+}
+
+#[test]
+fn test_lerp_consistent() {
+    assert_eq!(f32::lerp(f32::MAX, f32::MIN, f32::MIN), f32::MIN);
+    assert_eq!(f32::lerp(f32::MIN, f32::MAX, f32::MAX), f32::MAX);
+
+    // as long as t is finite, a/b can be infinite
+    assert_eq!(f32::lerp(f32::MAX, f32::NEG_INFINITY, f32::NEG_INFINITY), f32::NEG_INFINITY);
+    assert_eq!(f32::lerp(f32::MIN, f32::INFINITY, f32::INFINITY), f32::INFINITY);
+}
+
+#[test]
+fn test_lerp_nan_infinite() {
+    // non-finite t is not NaN if a/b different
+    assert!(!f32::lerp(f32::INFINITY, f32::MIN, f32::MAX).is_nan());
+    assert!(!f32::lerp(f32::NEG_INFINITY, f32::MIN, f32::MAX).is_nan());
+}
+
+#[test]
+fn test_lerp_values() {
+    // just a few basic values
+    assert_eq!(f32::lerp(0.25, 1.0, 2.0), 1.25);
+    assert_eq!(f32::lerp(0.50, 1.0, 2.0), 1.50);
+    assert_eq!(f32::lerp(0.75, 1.0, 2.0), 1.75);
+}
+
+#[test]
+fn test_lerp_monotonic() {
+    // near 0
+    let below_zero = f32::lerp(-f32::EPSILON, f32::MIN, f32::MAX);
+    let zero = f32::lerp(0.0, f32::MIN, f32::MAX);
+    let above_zero = f32::lerp(f32::EPSILON, f32::MIN, f32::MAX);
+    assert!(below_zero <= zero);
+    assert!(zero <= above_zero);
+    assert!(below_zero <= above_zero);
+
+    // near 0.5
+    let below_half = f32::lerp(0.5 - f32::EPSILON, f32::MIN, f32::MAX);
+    let half = f32::lerp(0.5, f32::MIN, f32::MAX);
+    let above_half = f32::lerp(0.5 + f32::EPSILON, f32::MIN, f32::MAX);
+    assert!(below_half <= half);
+    assert!(half <= above_half);
+    assert!(below_half <= above_half);
+
+    // near 1
+    let below_one = f32::lerp(1.0 - f32::EPSILON, f32::MIN, f32::MAX);
+    let one = f32::lerp(1.0, f32::MIN, f32::MAX);
+    let above_one = f32::lerp(1.0 + f32::EPSILON, f32::MIN, f32::MAX);
+    assert!(below_one <= one);
+    assert!(one <= above_one);
+    assert!(below_one <= above_one);
+}
diff --git a/library/std/src/f64.rs b/library/std/src/f64.rs
index 4c95df5ffe0..8c8cf73741b 100644
--- a/library/std/src/f64.rs
+++ b/library/std/src/f64.rs
@@ -879,6 +879,42 @@ impl f64 {
         0.5 * ((2.0 * self) / (1.0 - self)).ln_1p()
     }
 
+    /// Linear interpolation between `start` and `end`.
+    ///
+    /// This enables linear interpolation between `start` and `end`, where start is represented by
+    /// `self == 0.0` and `end` is represented by `self == 1.0`. This is the basis of all
+    /// "transition", "easing", or "step" functions; if you change `self` from 0.0 to 1.0
+    /// at a given rate, the result will change from `start` to `end` at a similar rate.
+    ///
+    /// Values below 0.0 or above 1.0 are allowed, allowing you to extrapolate values outside the
+    /// range from `start` to `end`. This also is useful for transition functions which might
+    /// move slightly past the end or start for a desired effect. Mathematically, the values
+    /// returned are equivalent to `start + self * (end - start)`, although we make a few specific
+    /// guarantees that are useful specifically to linear interpolation.
+    ///
+    /// These guarantees are:
+    ///
+    /// * If `start` and `end` are [finite], the value at 0.0 is always `start` and the
+    ///   value at 1.0 is always `end`. (exactness)
+    /// * If `start` and `end` are [finite], the values will always move in the direction from
+    ///   `start` to `end` (monotonicity)
+    /// * If `self` is [finite] and `start == end`, the value at any point will always be
+    ///   `start == end`. (consistency)
+    ///
+    /// [finite]: #method.is_finite
+    #[must_use = "method returns a new number and does not mutate the original value"]
+    #[unstable(feature = "float_interpolation", issue = "86269")]
+    pub fn lerp(self, start: f64, end: f64) -> f64 {
+        // consistent
+        if start == end {
+            start
+
+        // exact/monotonic
+        } else {
+            self.mul_add(end, (-self).mul_add(start, start))
+        }
+    }
+
     // Solaris/Illumos requires a wrapper around log, log2, and log10 functions
     // because of their non-standard behavior (e.g., log(-n) returns -Inf instead
     // of expected NaN).
diff --git a/library/std/src/f64/tests.rs b/library/std/src/f64/tests.rs
index 5c163cfe90e..04cb0109261 100644
--- a/library/std/src/f64/tests.rs
+++ b/library/std/src/f64/tests.rs
@@ -753,3 +753,58 @@ fn test_total_cmp() {
     assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&f64::INFINITY));
     assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&s_nan()));
 }
+
+#[test]
+fn test_lerp_exact() {
+    // simple values
+    assert_eq!(f64::lerp(0.0, 2.0, 4.0), 2.0);
+    assert_eq!(f64::lerp(1.0, 2.0, 4.0), 4.0);
+
+    // boundary values
+    assert_eq!(f64::lerp(0.0, f64::MIN, f64::MAX), f64::MIN);
+    assert_eq!(f64::lerp(1.0, f64::MIN, f64::MAX), f64::MAX);
+}
+
+#[test]
+fn test_lerp_consistent() {
+    assert_eq!(f64::lerp(f64::MAX, f64::MIN, f64::MIN), f64::MIN);
+    assert_eq!(f64::lerp(f64::MIN, f64::MAX, f64::MAX), f64::MAX);
+
+    // as long as t is finite, a/b can be infinite
+    assert_eq!(f64::lerp(f64::MAX, f64::NEG_INFINITY, f64::NEG_INFINITY), f64::NEG_INFINITY);
+    assert_eq!(f64::lerp(f64::MIN, f64::INFINITY, f64::INFINITY), f64::INFINITY);
+}
+
+#[test]
+fn test_lerp_nan_infinite() {
+    // non-finite t is not NaN if a/b different
+    assert!(!f64::lerp(f64::INFINITY, f64::MIN, f64::MAX).is_nan());
+    assert!(!f64::lerp(f64::NEG_INFINITY, f64::MIN, f64::MAX).is_nan());
+}
+
+#[test]
+fn test_lerp_values() {
+    // just a few basic values
+    assert_eq!(f64::lerp(0.25, 1.0, 2.0), 1.25);
+    assert_eq!(f64::lerp(0.50, 1.0, 2.0), 1.50);
+    assert_eq!(f64::lerp(0.75, 1.0, 2.0), 1.75);
+}
+
+#[test]
+fn test_lerp_monotonic() {
+    // near 0
+    let below_zero = f64::lerp(-f64::EPSILON, f64::MIN, f64::MAX);
+    let zero = f64::lerp(0.0, f64::MIN, f64::MAX);
+    let above_zero = f64::lerp(f64::EPSILON, f64::MIN, f64::MAX);
+    assert!(below_zero <= zero);
+    assert!(zero <= above_zero);
+    assert!(below_zero <= above_zero);
+
+    // near 1
+    let below_one = f64::lerp(1.0 - f64::EPSILON, f64::MIN, f64::MAX);
+    let one = f64::lerp(1.0, f64::MIN, f64::MAX);
+    let above_one = f64::lerp(1.0 + f64::EPSILON, f64::MIN, f64::MAX);
+    assert!(below_one <= one);
+    assert!(one <= above_one);
+    assert!(below_one <= above_one);
+}
diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs
index 6b2f49cfe48..664cc748ca6 100644
--- a/library/std/src/lib.rs
+++ b/library/std/src/lib.rs
@@ -268,6 +268,7 @@
 #![feature(exhaustive_patterns)]
 #![feature(extend_one)]
 #![cfg_attr(bootstrap, feature(extended_key_value_attributes))]
+#![feature(float_interpolation)]
 #![feature(fn_traits)]
 #![feature(format_args_nl)]
 #![feature(gen_future)]