1use std::sync::Arc;
9
10use arrow::array::{Array, AsArray, Float64Array};
11use arrow::compute::cast;
12use arrow::compute::kernels::{boolean, cmp, zip::zip};
13use arrow::datatypes::{DataType, Float64Type, Int64Type};
14
15use crate::NdArrowArray;
16use crate::compute::elementwise::{add, logical_and, logical_or, mul};
17use crate::compute::reduce::{Reduction, count, reduce};
18use crate::dimensions::Dimensions;
19use crate::error::{Result, nd_err};
20
21#[derive(Debug, Clone)]
22enum State {
23 Single(NdArrowArray),
25 Extreme(NdArrowArray),
27 Mean { sum: Vec<f64>, count: Vec<u64> },
29 Moments {
31 count: Vec<u64>,
32 mean: Vec<f64>,
33 m2: Vec<f64>,
34 },
35}
36
37#[derive(Debug, Clone)]
39pub struct PartialReduction {
40 op: Reduction,
41 dims: Dimensions,
42 input_type: DataType,
43 state: State,
44}
45
46impl PartialReduction {
47 pub fn new(a: &NdArrowArray, axes: Option<&[&str]>, op: Reduction) -> Result<Self> {
49 let f64_input = || crate::compute::cast(a, &DataType::Float64);
50 let (state, dims) = match op {
51 Reduction::Sum
52 | Reduction::Prod
53 | Reduction::Count
54 | Reduction::Any
55 | Reduction::All => {
56 let r = reduce(a, axes, op)?;
57 let dims = r.dims().clone();
58 (State::Single(r), dims)
59 }
60 Reduction::Min | Reduction::Max => {
61 let r = reduce(a, axes, op)?;
62 let dims = r.dims().clone();
63 (State::Extreme(r), dims)
64 }
65 Reduction::Mean => {
66 let x = f64_input()?;
67 let s = reduce(&x, axes, Reduction::Sum)?;
68 let n = count(&x, axes)?;
69 let dims = s.dims().clone();
70 (
71 State::Mean {
72 sum: f64_values(&s),
73 count: u64_values(&n),
74 },
75 dims,
76 )
77 }
78 Reduction::Var { .. } | Reduction::Std { .. } => {
79 let x = f64_input()?;
80 let n = count(&x, axes)?;
81 let mean = reduce(&x, axes, Reduction::Mean)?;
82 let var = reduce(&x, axes, Reduction::Var { ddof: 0 })?;
83 let dims = n.dims().clone();
84 let count = u64_values(&n);
85 let m2 = f64_values(&var)
86 .iter()
87 .zip(&count)
88 .map(|(v, &c)| if c == 0 { 0.0 } else { v * c as f64 })
89 .collect();
90 (
91 State::Moments {
92 count,
93 mean: f64_values(&mean),
94 m2,
95 },
96 dims,
97 )
98 }
99 Reduction::Median | Reduction::Quantile(_) | Reduction::ArgMin | Reduction::ArgMax => {
100 return Err(nd_err!(NotSupported, "{op:?} has no mergeable state"));
101 }
102 };
103 Ok(Self {
104 op,
105 dims,
106 input_type: a.data_type().clone(),
107 state,
108 })
109 }
110
111 pub fn dims(&self) -> &Dimensions {
113 &self.dims
114 }
115
116 pub fn merge(&self, other: &Self) -> Result<Self> {
118 if self.op != other.op || self.dims != other.dims {
119 return Err(nd_err!(
120 InvalidArgument,
121 "cannot merge {:?} over {} with {:?} over {}",
122 self.op,
123 self.dims,
124 other.op,
125 other.dims
126 ));
127 }
128 let state = match (&self.state, &other.state) {
129 (State::Single(a), State::Single(b)) => State::Single(match self.op {
130 Reduction::Prod => mul(a, b)?,
131 Reduction::Any => logical_or(a, b)?,
132 Reduction::All => logical_and(a, b)?,
133 _ => add(a, b)?,
134 }),
135 (State::Extreme(a), State::Extreme(b)) => {
136 State::Extreme(merge_extreme(a, b, self.op == Reduction::Max)?)
137 }
138 (State::Mean { sum: sa, count: ca }, State::Mean { sum: sb, count: cb }) => {
139 State::Mean {
140 sum: sa.iter().zip(sb).map(|(a, b)| a + b).collect(),
141 count: ca.iter().zip(cb).map(|(a, b)| a + b).collect(),
142 }
143 }
144 (
145 State::Moments {
146 count: na,
147 mean: ma,
148 m2: qa,
149 },
150 State::Moments {
151 count: nb,
152 mean: mb,
153 m2: qb,
154 },
155 ) => {
156 let mut count = Vec::with_capacity(na.len());
157 let mut mean = Vec::with_capacity(na.len());
158 let mut m2 = Vec::with_capacity(na.len());
159 for i in 0..na.len() {
160 let (n1, n2) = (na[i] as f64, nb[i] as f64);
161 let n = n1 + n2;
162 let (c, m, q) = if n2 == 0.0 {
163 (na[i], ma[i], qa[i])
164 } else if n1 == 0.0 {
165 (nb[i], mb[i], qb[i])
166 } else {
167 let delta = mb[i] - ma[i];
168 (
169 na[i] + nb[i],
170 ma[i] + delta * n2 / n,
171 qa[i] + qb[i] + delta * delta * n1 * n2 / n,
172 )
173 };
174 count.push(c);
175 mean.push(m);
176 m2.push(q);
177 }
178 State::Moments { count, mean, m2 }
179 }
180 _ => unreachable!("same op gives the same state"),
181 };
182 Ok(Self {
183 op: self.op,
184 dims: self.dims.clone(),
185 input_type: self.input_type.clone(),
186 state,
187 })
188 }
189
190 pub fn finish(&self) -> Result<NdArrowArray> {
192 let float_out = |values: Vec<Option<f64>>| -> Result<NdArrowArray> {
193 let mut out: Arc<dyn Array> = Arc::new(Float64Array::from(values));
194 if matches!(self.input_type, DataType::Float32 | DataType::Float16) {
195 out = cast(out.as_ref(), &DataType::Float32)?;
196 }
197 NdArrowArray::try_new(out, self.dims.clone())
198 };
199 match &self.state {
200 State::Single(a) | State::Extreme(a) => Ok(a.clone()),
201 State::Mean { sum, count } => float_out(
202 sum.iter()
203 .zip(count)
204 .map(|(&s, &n)| (n > 0).then(|| s / n as f64))
205 .collect(),
206 ),
207 State::Moments { count, m2, .. } => {
208 let (ddof, sqrt) = match self.op {
209 Reduction::Var { ddof } => (ddof, false),
210 Reduction::Std { ddof } => (ddof, true),
211 _ => unreachable!(),
212 };
213 float_out(
214 count
215 .iter()
216 .zip(m2)
217 .map(|(&n, &q)| {
218 (n as usize > ddof).then(|| {
219 let v = q / (n as usize - ddof) as f64;
220 if sqrt { v.sqrt() } else { v }
221 })
222 })
223 .collect(),
224 )
225 }
226 }
227 }
228}
229
230pub fn merge_all(partials: &[PartialReduction]) -> Result<PartialReduction> {
232 match partials {
233 [] => Err(nd_err!(InvalidArgument, "no partial reductions to merge")),
234 [single] => Ok(single.clone()),
235 many => {
236 let (left, right) = many.split_at(many.len() / 2);
237 merge_all(left)?.merge(&merge_all(right)?)
238 }
239 }
240}
241
242fn f64_values(a: &NdArrowArray) -> Vec<f64> {
243 let values = cast(a.values().as_ref(), &DataType::Float64).expect("numeric");
244 values
245 .as_primitive::<Float64Type>()
246 .iter()
247 .map(|v| v.unwrap_or(0.0))
248 .collect()
249}
250
251fn u64_values(a: &NdArrowArray) -> Vec<u64> {
252 a.values()
253 .as_primitive::<Int64Type>()
254 .values()
255 .iter()
256 .map(|&v| v as u64)
257 .collect()
258}
259
260fn merge_extreme(a: &NdArrowArray, b: &NdArrowArray, is_max: bool) -> Result<NdArrowArray> {
262 let (x, y) = (a.values(), b.values());
263 let better = if is_max {
264 cmp::gt(y, x)?
265 } else {
266 cmp::lt(y, x)?
267 };
268 let a_missing = arrow::compute::is_null(x.as_ref())?;
269 let b_present = arrow::compute::is_not_null(y.as_ref())?;
270 let take_b = boolean::or_kleene(&a_missing, &boolean::and_kleene(&b_present, &better)?)?;
271 let values = zip(&take_b, y, x)?;
272 NdArrowArray::try_new(values, a.dims().clone())
273}
274
275#[cfg(test)]
276mod tests {
277 use arrow::array::Float64Array;
278
279 use super::*;
280 use crate::compute::shape::{Selector, Slice, isel};
281
282 fn data() -> NdArrowArray {
283 let values: Vec<Option<f64>> = (0..60)
284 .map(|i| (i % 7 != 3).then(|| ((i * 37) % 23) as f64 - 11.0))
285 .collect();
286 NdArrowArray::try_new(
287 Arc::new(Float64Array::from(values)),
288 Dimensions::try_new([("time", 12), ("x", 5)]).unwrap(),
289 )
290 .unwrap()
291 }
292
293 fn chunks(a: &NdArrowArray) -> Vec<NdArrowArray> {
294 [(0, 5), (5, 6), (6, 12)]
295 .iter()
296 .map(|&(s, e)| isel(a, &[("time", Selector::Slice(Slice::range(s, e)))]).unwrap())
297 .collect()
298 }
299
300 fn close(a: &NdArrowArray, b: &NdArrowArray) -> bool {
301 let x = cast(a.values().as_ref(), &DataType::Float64).unwrap();
302 let y = cast(b.values().as_ref(), &DataType::Float64).unwrap();
303 let (x, y) = (
304 x.as_primitive::<Float64Type>(),
305 y.as_primitive::<Float64Type>(),
306 );
307 x.len() == y.len()
308 && x.iter().zip(y.iter()).all(|(p, q)| match (p, q) {
309 (None, None) => true,
310 (Some(p), Some(q)) => (p - q).abs() < 1e-9,
311 _ => false,
312 })
313 }
314
315 #[test]
316 fn merged_chunks_equal_full_reduction() {
317 let a = data();
318 let ops = [
319 Reduction::Sum,
320 Reduction::Prod,
321 Reduction::Count,
322 Reduction::Mean,
323 Reduction::Min,
324 Reduction::Max,
325 Reduction::Var { ddof: 0 },
326 Reduction::Std { ddof: 1 },
327 ];
328 for op in ops {
329 for axes in [Some(&["time"][..]), None] {
330 let partials: Vec<PartialReduction> = chunks(&a)
331 .iter()
332 .map(|c| PartialReduction::new(c, axes, op).unwrap())
333 .collect();
334 let merged = merge_all(&partials).unwrap().finish().unwrap();
335 let full = reduce(&a, axes, op).unwrap();
336 assert!(close(&merged, &full), "{op:?} over {axes:?}");
337 }
338 }
339 }
340
341 #[test]
342 fn unsupported_and_mismatched() {
343 let a = data();
344 assert!(PartialReduction::new(&a, None, Reduction::Median).is_err());
345 let p = PartialReduction::new(&a, None, Reduction::Sum).unwrap();
346 let q = PartialReduction::new(&a, Some(&["x"]), Reduction::Sum).unwrap();
347 assert!(p.merge(&q).is_err());
348 }
349}