1use std::sync::Arc;
4
5use arrow::array::{Array, ArrayRef, AsArray, BooleanArray, Float64Array};
6use arrow::compute::cast as arrow_cast;
7use arrow::datatypes::{DataType, Float64Type};
8
9use crate::NdArrowArray;
10use crate::compute::elementwise::where_;
11use crate::compute::search::partition_point;
12use crate::compute::shape::take;
13use crate::dimensions::{Dimension, Dimensions};
14use crate::error::{Result, nd_err};
15
16fn to_f64(values: &ArrayRef) -> Result<Float64Array> {
17 Ok(arrow_cast(values.as_ref(), &DataType::Float64)?
18 .as_primitive::<Float64Type>()
19 .clone())
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum InterpMethod {
25 #[default]
27 Linear,
28 Nearest,
30}
31
32pub fn interp(
40 a: &NdArrowArray,
41 dim: &str,
42 x: &NdArrowArray,
43 new_x: &NdArrowArray,
44) -> Result<NdArrowArray> {
45 interp_with(a, dim, x, new_x, InterpMethod::Linear)
46}
47
48pub fn interp_with(
50 a: &NdArrowArray,
51 dim: &str,
52 x: &NdArrowArray,
53 new_x: &NdArrowArray,
54 method: InterpMethod,
55) -> Result<NdArrowArray> {
56 let plan = target_plan(a, dim, x, new_x)?;
57 match method {
58 InterpMethod::Linear => interp_linear(a, dim, &plan),
59 InterpMethod::Nearest => interp_nearest(a, dim, &plan),
60 }
61}
62
63fn target_plan(
65 a: &NdArrowArray,
66 dim: &str,
67 x: &NdArrowArray,
68 new_x: &NdArrowArray,
69) -> Result<Vec<Option<(usize, f64)>>> {
70 let axis = a.dims().axis(dim)?;
71 let n = a.dims().as_slice()[axis].size();
72 if x.rank() != 1 || x.len() != n {
73 return Err(nd_err!(
74 InvalidArgument,
75 "interp coordinates must be 1-D with {n} values along '{dim}'"
76 ));
77 }
78 if new_x.rank() != 1 {
79 return Err(nd_err!(InvalidArgument, "interp targets must be 1-D"));
80 }
81 let xs = to_f64(x.values())?;
82 if xs.null_count() > 0 || xs.values().windows(2).any(|w| w[1] <= w[0]) {
83 return Err(nd_err!(
84 InvalidArgument,
85 "interp coordinates must be non-null and strictly increasing"
86 ));
87 }
88 let xs = xs.values();
89 let targets = to_f64(new_x.values())?;
90
91 let plan: Vec<Option<(usize, f64)>> = targets
92 .iter()
93 .map(|t| {
94 let t = t?;
95 if n == 0 || t < xs[0] || t > xs[n - 1] {
96 return None;
97 }
98 let upper = partition_point(n, |i| xs[i] < t);
99 if xs[upper] == t {
100 return Some((upper, 0.0));
101 }
102 let lower = upper - 1;
103 Some((lower, (t - xs[lower]) / (xs[upper] - xs[lower])))
104 })
105 .collect();
106 Ok(plan)
107}
108
109fn interp_nearest(
110 a: &NdArrowArray,
111 dim: &str,
112 plan: &[Option<(usize, f64)>],
113) -> Result<NdArrowArray> {
114 let indices: Vec<isize> = plan
115 .iter()
116 .map(|p| p.map_or(0, |(lower, w)| if w <= 0.5 { lower } else { lower + 1 }) as isize)
117 .collect();
118 if plan.iter().all(Option::is_some) {
119 return take(a, dim, &indices);
120 }
121 let picked = take(a, dim, &indices)?;
122 let valid = NdArrowArray::try_new(
123 Arc::new(BooleanArray::from_iter(
124 plan.iter().map(|p| Some(p.is_some())),
125 )),
126 Dimensions::try_new([(dim, plan.len())])?,
127 )?;
128 where_(
129 &valid,
130 &picked,
131 &NdArrowArray::new_null_scalar(a.data_type()),
132 )
133}
134
135fn interp_linear(
136 a: &NdArrowArray,
137 dim: &str,
138 plan: &[Option<(usize, f64)>],
139) -> Result<NdArrowArray> {
140 let axis = a.dims().axis(dim)?;
141 let n = a.dims().as_slice()[axis].size();
142
143 let values = to_f64(a.values())?;
144 let shape = a.shape();
145 let outer: usize = shape[..axis].iter().product();
146 let inner: usize = shape[axis + 1..].iter().product();
147 let m = plan.len();
148 let mut out: Vec<Option<f64>> = Vec::with_capacity(outer * m * inner);
149 let get = |p: usize| values.is_valid(p).then(|| values.value(p));
150 for o in 0..outer {
151 for step in plan {
152 for j in 0..inner {
153 out.push(step.and_then(|(lower, w)| {
154 let low = get((o * n + lower) * inner + j)?;
155 if w == 0.0 {
156 return Some(low);
157 }
158 let high = get((o * n + lower + 1) * inner + j)?;
159 Some(low + (high - low) * w)
160 }));
161 }
162 }
163 }
164
165 let mut result: ArrayRef = Arc::new(Float64Array::from(out));
166 if matches!(a.data_type(), DataType::Float32 | DataType::Float16) {
167 result = arrow_cast(result.as_ref(), &DataType::Float32)?;
168 }
169 let dims = a.dims().iter().map(|d| {
170 if d.name() == dim {
171 Dimension::new(dim, m)
172 } else {
173 d.clone()
174 }
175 });
176 Ok(NdArrowArray::try_new(result, Dimensions::try_new(dims)?)?
177 .with_attributes(a.attributes().clone()))
178}
179
180#[cfg(test)]
181mod tests {
182 use arrow::array::Int32Array;
183
184 use super::*;
185
186 fn nd(values: ArrayRef, dims: &[(&str, usize)]) -> NdArrowArray {
187 NdArrowArray::try_new(values, Dimensions::try_new(dims.iter().copied()).unwrap()).unwrap()
188 }
189
190 #[test]
191 fn interpolates_each_lane() {
192 let a = nd(
193 Arc::new(Int32Array::from(vec![0, 10, 20, 100, 110, 120])),
194 &[("y", 2), ("x", 3)],
195 );
196 let x = nd(
197 Arc::new(Float64Array::from(vec![0.0, 1.0, 3.0])),
198 &[("x", 3)],
199 );
200 let new_x = nd(
201 Arc::new(Float64Array::from(vec![0.5, 2.0, 3.0, 4.0])),
202 &[("x", 4)],
203 );
204 let out = interp(&a, "x", &x, &new_x).unwrap();
205 assert_eq!(out.shape(), vec![2, 4]);
206 let v: Vec<Option<f64>> = out.values().as_primitive::<Float64Type>().iter().collect();
207 assert_eq!(
208 v,
209 vec![
210 Some(5.0),
211 Some(15.0),
212 Some(20.0),
213 None,
214 Some(105.0),
215 Some(115.0),
216 Some(120.0),
217 None
218 ]
219 );
220 }
221
222 #[test]
223 fn nearest_keeps_type() {
224 let a = nd(Arc::new(Int32Array::from(vec![10, 20, 30])), &[("x", 3)]);
225 let x = nd(
226 Arc::new(Float64Array::from(vec![0.0, 1.0, 2.0])),
227 &[("x", 3)],
228 );
229 let new_x = nd(
230 Arc::new(Float64Array::from(vec![0.4, 0.5, 1.6, 9.0])),
231 &[("x", 4)],
232 );
233 let out = interp_with(&a, "x", &x, &new_x, InterpMethod::Nearest).unwrap();
234 let v: Vec<Option<i32>> = out
235 .values()
236 .as_primitive::<arrow::datatypes::Int32Type>()
237 .iter()
238 .collect();
239 assert_eq!(v, vec![Some(10), Some(10), Some(30), None]);
240 }
241
242 #[test]
243 fn rejects_unsorted_coordinates() {
244 let a = nd(Arc::new(Int32Array::from(vec![1, 2])), &[("x", 2)]);
245 let x = nd(Arc::new(Float64Array::from(vec![1.0, 0.0])), &[("x", 2)]);
246 assert!(interp(&a, "x", &x, &x).is_err());
247 }
248}