Add methods to sample curves from IntoIterator types (#14815)

# Objective

Citing @mweatherley 

> As mentioned before, a multi-sampling function in the API which takes
an iterator is probably something we want (e.g. `sample_iter(iter: impl
IntoIterator<Item = f32>) -> impl IntoIterator<Item = T> { //... }`, but
there are some design choices to be made on the details (e.g. does this
filter out points that aren't in the domain? does it do sorting? etc.)

## Solution

I think the most flexible solution for end users is to expose all the
`sample_...` functions with an `iter` equivalent, so we'll have

- `sample_iter`
- `sample_iter_unchecked`
- `sample_iter_clamped`

Answering some questions from the original idea:

> does this filter out points that aren't in the domain?

With the methods the user has the choice to just sample or if they want
to filter out invalid types us `sample_iter` and then apply `filter_map`
to the iterator returned themselves.

> does it do sorting?

I think it's the same thing. If the user wants it, they need to do it
themselves by either collecting and sorting a `Vec` or using
`itertools`. I think there is a legit use case for "please sample me
this collection of points that are unordered" and we would destroy it if
we take away to much agency from users by sorting for them

## Testing

- Added a test which covers all three methods
This commit is contained in:
Robert Walter 2024-08-26 18:08:41 +00:00 committed by GitHub
parent 3540b87e17
commit 96f1fd73cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -51,6 +51,44 @@ pub trait Curve<T> {
self.sample_unchecked(t)
}
/// Sample a collection of `n >= 0` points on this curve at the parameter values `t_n`,
/// returning `None` if the point is outside of the curve's domain.
///
/// The samples are returned in the same order as the parameter values `t_n` were provided and
/// will include all results. This leaves the responsibility for things like filtering and
/// sorting to the user for maximum flexibility.
fn sample_iter(&self, iter: impl IntoIterator<Item = f32>) -> impl Iterator<Item = Option<T>> {
iter.into_iter().map(|t| self.sample(t))
}
/// Sample a collection of `n >= 0` points on this curve at the parameter values `t_n`,
/// extracting the associated values. This is the unchecked version of sampling, which should
/// only be used if the sample times `t_n` are already known to lie within the curve's domain.
///
/// Values sampled from outside of a curve's domain are generally considered invalid; data
/// which is nonsensical or otherwise useless may be returned in such a circumstance, and
/// extrapolation beyond a curve's domain should not be relied upon.
///
/// The samples are returned in the same order as the parameter values `t_n` were provided and
/// will include all results. This leaves the responsibility for things like filtering and
/// sorting to the user for maximum flexibility.
fn sample_iter_unchecked(
&self,
iter: impl IntoIterator<Item = f32>,
) -> impl Iterator<Item = T> {
iter.into_iter().map(|t| self.sample_unchecked(t))
}
/// Sample a collection of `n >= 0` points on this curve at the parameter values `t_n`,
/// clamping `t_n` to lie inside the domain of the curve.
///
/// The samples are returned in the same order as the parameter values `t_n` were provided and
/// will include all results. This leaves the responsibility for things like filtering and
/// sorting to the user for maximum flexibility.
fn sample_iter_clamped(&self, iter: impl IntoIterator<Item = f32>) -> impl Iterator<Item = T> {
iter.into_iter().map(|t| self.sample_clamped(t))
}
/// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the
/// sample at time `t` for this curve is `x`, the value at time `t` on the new curve will be
/// `f(x)`.
@ -1124,4 +1162,38 @@ mod tests {
assert_abs_diff_eq!(resampled_curve.domain().start(), 1.0);
assert_abs_diff_eq!(resampled_curve.domain().end(), 512.0);
}
#[test]
fn sample_iterators() {
let times = [-0.5, 0.0, 0.5, 1.0, 1.5];
let curve = function_curve(Interval::EVERYWHERE, |t| t * 3.0 + 1.0);
let samples = curve.sample_iter_unchecked(times).collect::<Vec<_>>();
let [y0, y1, y2, y3, y4] = samples.try_into().unwrap();
assert_eq!(y0, -0.5 * 3.0 + 1.0);
assert_eq!(y1, 0.0 * 3.0 + 1.0);
assert_eq!(y2, 0.5 * 3.0 + 1.0);
assert_eq!(y3, 1.0 * 3.0 + 1.0);
assert_eq!(y4, 1.5 * 3.0 + 1.0);
let finite_curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let samples = finite_curve.sample_iter(times).collect::<Vec<_>>();
let [y0, y1, y2, y3, y4] = samples.try_into().unwrap();
assert_eq!(y0, None);
assert_eq!(y1, Some(0.0 * 3.0 + 1.0));
assert_eq!(y2, Some(0.5 * 3.0 + 1.0));
assert_eq!(y3, Some(1.0 * 3.0 + 1.0));
assert_eq!(y4, None);
let samples = finite_curve.sample_iter_clamped(times).collect::<Vec<_>>();
let [y0, y1, y2, y3, y4] = samples.try_into().unwrap();
assert_eq!(y0, 0.0 * 3.0 + 1.0);
assert_eq!(y1, 0.0 * 3.0 + 1.0);
assert_eq!(y2, 0.5 * 3.0 + 1.0);
assert_eq!(y3, 1.0 * 3.0 + 1.0);
assert_eq!(y4, 1.0 * 3.0 + 1.0);
}
}