use crate::grid::{AxisDirection, GridError, OutOfRange};
#[must_use]
pub(crate) fn locate(xs: &[f64], x: f64) -> (usize, f64) {
let last = xs.len() - 1;
if !x.is_finite() {
return if x.is_sign_positive() {
(last - 1, 1.0)
} else {
(0, 0.0)
};
}
if x <= xs[0] {
return (0, 0.0);
}
if x >= xs[last] {
return (last - 1, 1.0);
}
let upper = xs.partition_point(|v| *v <= x);
let low = upper - 1;
let denom = xs[upper] - xs[low];
let fraction = if denom == 0.0 {
0.0
} else {
(x - xs[low]) / denom
};
(low, fraction.clamp(0.0, 1.0))
}
#[must_use]
pub(crate) fn locate_uniform(start: f64, step: f64, count: usize, x: f64) -> (usize, f64) {
if count < 2 {
return (0, 0.0);
}
let last = count - 1;
let end = start + step * last as f64;
if !x.is_finite() {
return if x.is_sign_positive() {
(last - 1, 1.0)
} else {
(0, 0.0)
};
}
if x <= start {
return (0, 0.0);
}
if x >= end {
return (last - 1, 1.0);
}
let pos = (x - start) / step;
let low = pos.floor() as usize;
let clamped_low = low.min(last - 1);
let fraction = (pos - clamped_low as f64).clamp(0.0, 1.0);
(clamped_low, fraction)
}
#[must_use]
pub(crate) fn lerp(y0: f64, y1: f64, t: f64) -> f64 {
y0 + t * (y1 - y0)
}
#[allow(dead_code)]
#[must_use]
pub(crate) fn bilerp(v00: f64, v01: f64, v10: f64, v11: f64, tx: f64, ty: f64) -> f64 {
lerp(lerp(v00, v01, ty), lerp(v10, v11, ty), tx)
}
#[allow(dead_code)]
#[allow(clippy::too_many_arguments)]
#[must_use]
pub(crate) fn trilerp(
v000: f64,
v001: f64,
v010: f64,
v011: f64,
v100: f64,
v101: f64,
v110: f64,
v111: f64,
tx: f64,
ty: f64,
tz: f64,
) -> f64 {
let c00 = lerp(v000, v001, tz);
let c01 = lerp(v010, v011, tz);
let c10 = lerp(v100, v101, tz);
let c11 = lerp(v110, v111, tz);
bilerp(c00, c01, c10, c11, tx, ty)
}
pub fn validate_axis(name: &'static str, xs: &[f64]) -> Result<AxisDirection, GridError> {
if xs.len() < 2 {
return Err(GridError::TooFewSamples {
axis: name,
len: xs.len(),
});
}
for (index, &v) in xs.iter().enumerate() {
if !v.is_finite() {
return Err(GridError::NonFinite { axis: name, index });
}
}
let ascending = xs[1] > xs[0];
for i in 1..xs.len() {
let monotone = if ascending {
xs[i] > xs[i - 1]
} else {
xs[i] < xs[i - 1]
};
if !monotone {
return Err(GridError::NotMonotonic {
axis: name,
at_index: i,
});
}
}
Ok(if ascending {
AxisDirection::Ascending
} else {
AxisDirection::Descending
})
}
#[must_use]
pub(crate) fn axis_range(xs: &[f64], dir: AxisDirection) -> (f64, f64) {
match dir {
AxisDirection::Ascending => (xs[0], xs[xs.len() - 1]),
AxisDirection::Descending => (xs[xs.len() - 1], xs[0]),
}
}
#[must_use]
pub(crate) fn locate_dir(xs: &[f64], x: f64, dir: AxisDirection) -> (usize, f64) {
match dir {
AxisDirection::Ascending => locate(xs, x),
AxisDirection::Descending => {
let last = xs.len() - 1;
if !x.is_finite() {
return if x.is_sign_positive() {
(0, 0.0)
} else {
(last - 1, 1.0)
};
}
if x >= xs[0] {
return (0, 0.0);
}
if x <= xs[last] {
return (last - 1, 1.0);
}
let upper = xs.partition_point(|v| *v > x);
let low = upper - 1;
let denom = xs[low] - xs[upper]; let fraction = if denom == 0.0 {
0.0
} else {
(xs[low] - x) / denom
};
(low, fraction.clamp(0.0, 1.0))
}
}
}
pub(crate) fn check_oor(
v: f64,
lo: f64,
hi: f64,
oor: OutOfRange,
axis: &'static str,
) -> Result<bool, GridError> {
if v >= lo && v <= hi {
return Ok(true);
}
match oor {
OutOfRange::ClampToEndpoints => Ok(true),
OutOfRange::Zero => Ok(false),
OutOfRange::Error => Err(GridError::OutOfRange {
axis,
value: v,
lo,
hi,
}),
}
}
pub fn linear_1d(
xs: &[f64],
ys: &[f64],
x: f64,
oor: OutOfRange,
dir: AxisDirection,
) -> Result<f64, GridError> {
let (lo, hi) = axis_range(xs, dir);
if !check_oor(x, lo, hi, oor, "x")? {
return Ok(0.0);
}
let (i, t) = locate_dir(xs, x, dir);
Ok(lerp(ys[i], ys[i + 1], t))
}
#[allow(clippy::too_many_arguments)]
pub fn bilinear(
xs: &[f64],
ys: &[f64],
table: &[&[f64]],
x: f64,
y: f64,
oor_x: OutOfRange,
oor_y: OutOfRange,
dir_x: AxisDirection,
dir_y: AxisDirection,
) -> Result<f64, GridError> {
let (x_lo, x_hi) = axis_range(xs, dir_x);
let (y_lo, y_hi) = axis_range(ys, dir_y);
if !check_oor(x, x_lo, x_hi, oor_x, "x")? {
return Ok(0.0);
}
if !check_oor(y, y_lo, y_hi, oor_y, "y")? {
return Ok(0.0);
}
let (ix, tx) = locate_dir(xs, x, dir_x);
let (iy, ty) = locate_dir(ys, y, dir_y);
let f00 = table[iy][ix];
let f10 = table[iy][ix + 1];
let f01 = table[iy + 1][ix];
let f11 = table[iy + 1][ix + 1];
Ok(bilinear_unit(f00, f10, f01, f11, tx, ty))
}
#[must_use]
pub fn bilinear_unit(f00: f64, f10: f64, f01: f64, f11: f64, tx: f64, ty: f64) -> f64 {
lerp(lerp(f00, f10, tx), lerp(f01, f11, tx), ty)
}
#[allow(clippy::too_many_arguments)]
pub fn trilinear(
xs: &[f64],
ys: &[f64],
zs: &[f64],
table: &[f64],
x: f64,
y: f64,
z: f64,
oor_x: OutOfRange,
oor_y: OutOfRange,
oor_z: OutOfRange,
dir_x: AxisDirection,
dir_y: AxisDirection,
dir_z: AxisDirection,
) -> Result<f64, GridError> {
let (x_lo, x_hi) = axis_range(xs, dir_x);
let (y_lo, y_hi) = axis_range(ys, dir_y);
let (z_lo, z_hi) = axis_range(zs, dir_z);
if !check_oor(x, x_lo, x_hi, oor_x, "x")? {
return Ok(0.0);
}
if !check_oor(y, y_lo, y_hi, oor_y, "y")? {
return Ok(0.0);
}
if !check_oor(z, z_lo, z_hi, oor_z, "z")? {
return Ok(0.0);
}
let (ix, tx) = locate_dir(xs, x, dir_x);
let (iy, ty) = locate_dir(ys, y, dir_y);
let (iz, tz) = locate_dir(zs, z, dir_z);
let nx = xs.len();
let ny = ys.len();
let idx = |iz: usize, iy: usize, ix: usize| (iz * ny + iy) * nx + ix;
let v000 = table[idx(iz, iy, ix)];
let v100 = table[idx(iz, iy, ix + 1)];
let v010 = table[idx(iz, iy + 1, ix)];
let v110 = table[idx(iz, iy + 1, ix + 1)];
let v001 = table[idx(iz + 1, iy, ix)];
let v101 = table[idx(iz + 1, iy, ix + 1)];
let v011 = table[idx(iz + 1, iy + 1, ix)];
let v111 = table[idx(iz + 1, iy + 1, ix + 1)];
Ok(trilinear_unit(
v000, v100, v010, v110, v001, v101, v011, v111, tx, ty, tz,
))
}
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn trilinear_unit(
v000: f64,
v100: f64,
v010: f64,
v110: f64,
v001: f64,
v101: f64,
v011: f64,
v111: f64,
tx: f64,
ty: f64,
tz: f64,
) -> f64 {
let c0 = bilinear_unit(v000, v100, v010, v110, tx, ty);
let c1 = bilinear_unit(v001, v101, v011, v111, tx, ty);
lerp(c0, c1, tz)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locate_clamps_to_bounds() {
let xs = [1.0, 2.0, 4.0];
assert_eq!(locate(&xs, -1.0), (0, 0.0));
assert_eq!(locate(&xs, 10.0), (1, 1.0));
}
#[test]
fn locate_dir_descending_midpoint() {
let xs = [10.0, 5.0, 0.0];
let (i, t) = locate_dir(&xs, 7.5, AxisDirection::Descending);
assert_eq!(i, 0);
assert!((t - 0.5).abs() < 1e-12);
}
#[test]
fn locate_dir_descending_boundary() {
let xs = [10.0, 5.0, 0.0];
assert_eq!(locate_dir(&xs, 15.0, AxisDirection::Descending), (0, 0.0));
assert_eq!(locate_dir(&xs, -5.0, AxisDirection::Descending), (1, 1.0));
}
#[test]
fn validate_axis_detects_direction() {
let dir = validate_axis("x", &[1.0, 2.0, 3.0]).unwrap();
assert_eq!(dir, AxisDirection::Ascending);
let dir = validate_axis("y", &[3.0, 2.0, 1.0]).unwrap();
assert_eq!(dir, AxisDirection::Descending);
}
#[test]
fn bilinear_unit_x_first_convention() {
let v = bilinear_unit(0.0, 1.0, 0.0, 1.0, 0.5, 0.5);
assert!((v - 0.5).abs() < 1e-12);
let v = bilinear_unit(0.0, 2.0, 4.0, 8.0, 0.5, 0.0);
assert!((v - 1.0).abs() < 1e-12);
let v = bilinear_unit(0.0, 2.0, 4.0, 8.0, 0.5, 1.0);
assert!((v - 6.0).abs() < 1e-12);
}
#[test]
fn trilinear_unit_midpoint() {
let v = trilerp(0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 0.5, 0.5, 0.5);
assert_eq!(v, 7.0);
}
#[test]
fn bilinear_row_slice_api() {
let xs = [400.0, 500.0];
let ys = [0.0, 1.0];
let row0: &[f64] = &[1.0, 2.0];
let row1: &[f64] = &[3.0, 4.0];
let table: &[&[f64]] = &[row0, row1];
let v = bilinear(
&xs,
&ys,
table,
450.0,
0.0,
OutOfRange::ClampToEndpoints,
OutOfRange::ClampToEndpoints,
AxisDirection::Ascending,
AxisDirection::Ascending,
)
.unwrap();
assert!((v - 1.5).abs() < 1e-12);
}
}