Skip to content

Commit 282ff49

Browse files
m-ou-segitbot
authored and
gitbot
committed
Rollup merge of rust-lang#85925 - clarfonthey:lerp, r=m-ou-se
Linear interpolation rust-lang#71016 is a previous attempt at implementation that was closed by the author. I decided to reuse the feature request issue (rust-lang#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.
2 parents 0a4352e + b1df2b7 commit 282ff49

File tree

5 files changed

+191
-0
lines changed

5 files changed

+191
-0
lines changed

std/src/f32.rs

+36
Original file line numberDiff line numberDiff line change
@@ -876,4 +876,40 @@ impl f32 {
876876
pub fn atanh(self) -> f32 {
877877
0.5 * ((2.0 * self) / (1.0 - self)).ln_1p()
878878
}
879+
880+
/// Linear interpolation between `start` and `end`.
881+
///
882+
/// This enables linear interpolation between `start` and `end`, where start is represented by
883+
/// `self == 0.0` and `end` is represented by `self == 1.0`. This is the basis of all
884+
/// "transition", "easing", or "step" functions; if you change `self` from 0.0 to 1.0
885+
/// at a given rate, the result will change from `start` to `end` at a similar rate.
886+
///
887+
/// Values below 0.0 or above 1.0 are allowed, allowing you to extrapolate values outside the
888+
/// range from `start` to `end`. This also is useful for transition functions which might
889+
/// move slightly past the end or start for a desired effect. Mathematically, the values
890+
/// returned are equivalent to `start + self * (end - start)`, although we make a few specific
891+
/// guarantees that are useful specifically to linear interpolation.
892+
///
893+
/// These guarantees are:
894+
///
895+
/// * If `start` and `end` are [finite], the value at 0.0 is always `start` and the
896+
/// value at 1.0 is always `end`. (exactness)
897+
/// * If `start` and `end` are [finite], the values will always move in the direction from
898+
/// `start` to `end` (monotonicity)
899+
/// * If `self` is [finite] and `start == end`, the value at any point will always be
900+
/// `start == end`. (consistency)
901+
///
902+
/// [finite]: #method.is_finite
903+
#[must_use = "method returns a new number and does not mutate the original value"]
904+
#[unstable(feature = "float_interpolation", issue = "86269")]
905+
pub fn lerp(self, start: f32, end: f32) -> f32 {
906+
// consistent
907+
if start == end {
908+
start
909+
910+
// exact/monotonic
911+
} else {
912+
self.mul_add(end, (-self).mul_add(start, start))
913+
}
914+
}
879915
}

std/src/f32/tests.rs

+63
Original file line numberDiff line numberDiff line change
@@ -757,3 +757,66 @@ fn test_total_cmp() {
757757
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&f32::INFINITY));
758758
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&s_nan()));
759759
}
760+
761+
#[test]
762+
fn test_lerp_exact() {
763+
// simple values
764+
assert_eq!(f32::lerp(0.0, 2.0, 4.0), 2.0);
765+
assert_eq!(f32::lerp(1.0, 2.0, 4.0), 4.0);
766+
767+
// boundary values
768+
assert_eq!(f32::lerp(0.0, f32::MIN, f32::MAX), f32::MIN);
769+
assert_eq!(f32::lerp(1.0, f32::MIN, f32::MAX), f32::MAX);
770+
}
771+
772+
#[test]
773+
fn test_lerp_consistent() {
774+
assert_eq!(f32::lerp(f32::MAX, f32::MIN, f32::MIN), f32::MIN);
775+
assert_eq!(f32::lerp(f32::MIN, f32::MAX, f32::MAX), f32::MAX);
776+
777+
// as long as t is finite, a/b can be infinite
778+
assert_eq!(f32::lerp(f32::MAX, f32::NEG_INFINITY, f32::NEG_INFINITY), f32::NEG_INFINITY);
779+
assert_eq!(f32::lerp(f32::MIN, f32::INFINITY, f32::INFINITY), f32::INFINITY);
780+
}
781+
782+
#[test]
783+
fn test_lerp_nan_infinite() {
784+
// non-finite t is not NaN if a/b different
785+
assert!(!f32::lerp(f32::INFINITY, f32::MIN, f32::MAX).is_nan());
786+
assert!(!f32::lerp(f32::NEG_INFINITY, f32::MIN, f32::MAX).is_nan());
787+
}
788+
789+
#[test]
790+
fn test_lerp_values() {
791+
// just a few basic values
792+
assert_eq!(f32::lerp(0.25, 1.0, 2.0), 1.25);
793+
assert_eq!(f32::lerp(0.50, 1.0, 2.0), 1.50);
794+
assert_eq!(f32::lerp(0.75, 1.0, 2.0), 1.75);
795+
}
796+
797+
#[test]
798+
fn test_lerp_monotonic() {
799+
// near 0
800+
let below_zero = f32::lerp(-f32::EPSILON, f32::MIN, f32::MAX);
801+
let zero = f32::lerp(0.0, f32::MIN, f32::MAX);
802+
let above_zero = f32::lerp(f32::EPSILON, f32::MIN, f32::MAX);
803+
assert!(below_zero <= zero);
804+
assert!(zero <= above_zero);
805+
assert!(below_zero <= above_zero);
806+
807+
// near 0.5
808+
let below_half = f32::lerp(0.5 - f32::EPSILON, f32::MIN, f32::MAX);
809+
let half = f32::lerp(0.5, f32::MIN, f32::MAX);
810+
let above_half = f32::lerp(0.5 + f32::EPSILON, f32::MIN, f32::MAX);
811+
assert!(below_half <= half);
812+
assert!(half <= above_half);
813+
assert!(below_half <= above_half);
814+
815+
// near 1
816+
let below_one = f32::lerp(1.0 - f32::EPSILON, f32::MIN, f32::MAX);
817+
let one = f32::lerp(1.0, f32::MIN, f32::MAX);
818+
let above_one = f32::lerp(1.0 + f32::EPSILON, f32::MIN, f32::MAX);
819+
assert!(below_one <= one);
820+
assert!(one <= above_one);
821+
assert!(below_one <= above_one);
822+
}

std/src/f64.rs

+36
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,42 @@ impl f64 {
879879
0.5 * ((2.0 * self) / (1.0 - self)).ln_1p()
880880
}
881881

882+
/// Linear interpolation between `start` and `end`.
883+
///
884+
/// This enables linear interpolation between `start` and `end`, where start is represented by
885+
/// `self == 0.0` and `end` is represented by `self == 1.0`. This is the basis of all
886+
/// "transition", "easing", or "step" functions; if you change `self` from 0.0 to 1.0
887+
/// at a given rate, the result will change from `start` to `end` at a similar rate.
888+
///
889+
/// Values below 0.0 or above 1.0 are allowed, allowing you to extrapolate values outside the
890+
/// range from `start` to `end`. This also is useful for transition functions which might
891+
/// move slightly past the end or start for a desired effect. Mathematically, the values
892+
/// returned are equivalent to `start + self * (end - start)`, although we make a few specific
893+
/// guarantees that are useful specifically to linear interpolation.
894+
///
895+
/// These guarantees are:
896+
///
897+
/// * If `start` and `end` are [finite], the value at 0.0 is always `start` and the
898+
/// value at 1.0 is always `end`. (exactness)
899+
/// * If `start` and `end` are [finite], the values will always move in the direction from
900+
/// `start` to `end` (monotonicity)
901+
/// * If `self` is [finite] and `start == end`, the value at any point will always be
902+
/// `start == end`. (consistency)
903+
///
904+
/// [finite]: #method.is_finite
905+
#[must_use = "method returns a new number and does not mutate the original value"]
906+
#[unstable(feature = "float_interpolation", issue = "86269")]
907+
pub fn lerp(self, start: f64, end: f64) -> f64 {
908+
// consistent
909+
if start == end {
910+
start
911+
912+
// exact/monotonic
913+
} else {
914+
self.mul_add(end, (-self).mul_add(start, start))
915+
}
916+
}
917+
882918
// Solaris/Illumos requires a wrapper around log, log2, and log10 functions
883919
// because of their non-standard behavior (e.g., log(-n) returns -Inf instead
884920
// of expected NaN).

std/src/f64/tests.rs

+55
Original file line numberDiff line numberDiff line change
@@ -753,3 +753,58 @@ fn test_total_cmp() {
753753
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&f64::INFINITY));
754754
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&s_nan()));
755755
}
756+
757+
#[test]
758+
fn test_lerp_exact() {
759+
// simple values
760+
assert_eq!(f64::lerp(0.0, 2.0, 4.0), 2.0);
761+
assert_eq!(f64::lerp(1.0, 2.0, 4.0), 4.0);
762+
763+
// boundary values
764+
assert_eq!(f64::lerp(0.0, f64::MIN, f64::MAX), f64::MIN);
765+
assert_eq!(f64::lerp(1.0, f64::MIN, f64::MAX), f64::MAX);
766+
}
767+
768+
#[test]
769+
fn test_lerp_consistent() {
770+
assert_eq!(f64::lerp(f64::MAX, f64::MIN, f64::MIN), f64::MIN);
771+
assert_eq!(f64::lerp(f64::MIN, f64::MAX, f64::MAX), f64::MAX);
772+
773+
// as long as t is finite, a/b can be infinite
774+
assert_eq!(f64::lerp(f64::MAX, f64::NEG_INFINITY, f64::NEG_INFINITY), f64::NEG_INFINITY);
775+
assert_eq!(f64::lerp(f64::MIN, f64::INFINITY, f64::INFINITY), f64::INFINITY);
776+
}
777+
778+
#[test]
779+
fn test_lerp_nan_infinite() {
780+
// non-finite t is not NaN if a/b different
781+
assert!(!f64::lerp(f64::INFINITY, f64::MIN, f64::MAX).is_nan());
782+
assert!(!f64::lerp(f64::NEG_INFINITY, f64::MIN, f64::MAX).is_nan());
783+
}
784+
785+
#[test]
786+
fn test_lerp_values() {
787+
// just a few basic values
788+
assert_eq!(f64::lerp(0.25, 1.0, 2.0), 1.25);
789+
assert_eq!(f64::lerp(0.50, 1.0, 2.0), 1.50);
790+
assert_eq!(f64::lerp(0.75, 1.0, 2.0), 1.75);
791+
}
792+
793+
#[test]
794+
fn test_lerp_monotonic() {
795+
// near 0
796+
let below_zero = f64::lerp(-f64::EPSILON, f64::MIN, f64::MAX);
797+
let zero = f64::lerp(0.0, f64::MIN, f64::MAX);
798+
let above_zero = f64::lerp(f64::EPSILON, f64::MIN, f64::MAX);
799+
assert!(below_zero <= zero);
800+
assert!(zero <= above_zero);
801+
assert!(below_zero <= above_zero);
802+
803+
// near 1
804+
let below_one = f64::lerp(1.0 - f64::EPSILON, f64::MIN, f64::MAX);
805+
let one = f64::lerp(1.0, f64::MIN, f64::MAX);
806+
let above_one = f64::lerp(1.0 + f64::EPSILON, f64::MIN, f64::MAX);
807+
assert!(below_one <= one);
808+
assert!(one <= above_one);
809+
assert!(below_one <= above_one);
810+
}

std/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@
268268
#![feature(exhaustive_patterns)]
269269
#![feature(extend_one)]
270270
#![cfg_attr(bootstrap, feature(extended_key_value_attributes))]
271+
#![feature(float_interpolation)]
271272
#![feature(fn_traits)]
272273
#![feature(format_args_nl)]
273274
#![feature(gen_future)]

0 commit comments

Comments
 (0)