1use crate::compute::elementwise::{div, is_not_null_mask, mul, where_};
4use crate::compute::reduce::{Reduction, reduce, sum};
5use crate::compute::shape::{Selector, Slice, isel};
6use crate::dimensions::{Dimension, Dimensions};
7use crate::error::{Result, nd_err};
8use crate::{IntoScalar, NdArrowArray};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum CoarsenBoundary {
13 #[default]
15 Exact,
16 Trim,
18}
19
20pub fn coarsen(
25 a: &NdArrowArray,
26 windows: &[(&str, usize)],
27 op: Reduction,
28 boundary: CoarsenBoundary,
29) -> Result<NdArrowArray> {
30 let mut trimmed = a.clone();
31 for &(name, window) in windows {
32 if window == 0 {
33 return Err(nd_err!(
34 InvalidArgument,
35 "coarsen window must be at least 1"
36 ));
37 }
38 let size = a
39 .dims()
40 .size_of(name)
41 .ok_or_else(|| nd_err!(InvalidArgument, "dimension '{name}' is not in {}", a.dims()))?;
42 if size % window != 0 {
43 if boundary == CoarsenBoundary::Exact {
44 return Err(nd_err!(
45 ShapeMismatch,
46 "dimension '{name}' of size {size} is not a multiple of {window}"
47 ));
48 }
49 let keep = (size - size % window) as isize;
50 trimmed = isel(&trimmed, &[(name, Selector::Slice(Slice::range(0, keep)))])?;
51 }
52 }
53
54 let mut dims: Vec<Dimension> = Vec::new();
55 let mut window_dims: Vec<String> = Vec::new();
56 for dim in trimmed.dims().iter() {
57 match windows.iter().find(|(name, _)| *name == dim.name()) {
58 Some(&(name, window)) => {
59 let inner = format!("{name}__window");
60 if trimmed.dims().contains(&inner) {
61 return Err(nd_err!(
62 InvalidArgument,
63 "dimension '{inner}' already exists"
64 ));
65 }
66 dims.push(Dimension::new(name, dim.size() / window));
67 dims.push(Dimension::new(inner.as_str(), window));
68 window_dims.push(inner);
69 }
70 None => dims.push(dim.clone()),
71 }
72 }
73 let attrs = trimmed.attributes().clone();
74 let reshaped = trimmed.reshape(Dimensions::try_new(dims)?)?;
75 let names: Vec<&str> = window_dims.iter().map(String::as_str).collect();
76 Ok(reduce(&reshaped, Some(&names), op)?.with_attributes(attrs))
77}
78
79pub fn weighted_sum(
82 a: &NdArrowArray,
83 weights: &NdArrowArray,
84 axes: Option<&[&str]>,
85) -> Result<NdArrowArray> {
86 sum(&mul(a, weights)?, axes)
87}
88
89pub fn weighted_mean(
92 a: &NdArrowArray,
93 weights: &NdArrowArray,
94 axes: Option<&[&str]>,
95) -> Result<NdArrowArray> {
96 let numerator = weighted_sum(a, weights, axes)?;
97 let present = is_not_null_mask(a)?;
98 let zero = NdArrowArray::try_new_scalar(0.0f64.into_scalar())?;
99 let used_weights = where_(&present, weights, &zero)?;
100 let denominator = sum(&used_weights, axes)?;
101 let nonzero = crate::compute::ne(&denominator, &zero)?;
102 let null = NdArrowArray::new_null_scalar(&arrow::datatypes::DataType::Float64);
103 where_(&nonzero, &div(&numerator, &denominator)?, &null)
104}
105
106pub fn dot(a: &NdArrowArray, b: &NdArrowArray, dims: &[&str]) -> Result<NdArrowArray> {
111 for name in dims {
112 if !(a.dims().contains(name) && b.dims().contains(name)) {
113 return Err(nd_err!(
114 InvalidArgument,
115 "dot dimension '{name}' must be in both arrays"
116 ));
117 }
118 }
119 sum(&mul(a, b)?, Some(dims))
120}
121
122#[cfg(test)]
123mod tests {
124 use std::sync::Arc;
125
126 use arrow::array::{AsArray, Float64Array, Int32Array};
127 use arrow::datatypes::{Float64Type, Int64Type};
128
129 use super::*;
130
131 fn nd(values: Vec<f64>, dims: &[(&str, usize)]) -> NdArrowArray {
132 NdArrowArray::try_new(
133 Arc::new(Float64Array::from(values)),
134 Dimensions::try_new(dims.iter().copied()).unwrap(),
135 )
136 .unwrap()
137 }
138
139 fn f64s(a: &NdArrowArray) -> Vec<Option<f64>> {
140 a.values().as_primitive::<Float64Type>().iter().collect()
141 }
142
143 #[test]
144 fn coarsen_blocks() {
145 let a = NdArrowArray::try_new(
146 Arc::new(Int32Array::from_iter_values(0..16)),
147 Dimensions::try_new([("y", 4), ("x", 4)]).unwrap(),
148 )
149 .unwrap();
150 let c = coarsen(
151 &a,
152 &[("y", 2), ("x", 2)],
153 Reduction::Sum,
154 CoarsenBoundary::Exact,
155 )
156 .unwrap();
157 assert_eq!(
158 c.dims(),
159 &Dimensions::try_new([("y", 2), ("x", 2)]).unwrap()
160 );
161 let v: Vec<i64> = c.values().as_primitive::<Int64Type>().values().to_vec();
162 assert_eq!(v, vec![10, 18, 42, 50]);
163
164 assert!(coarsen(&a, &[("x", 3)], Reduction::Sum, CoarsenBoundary::Exact).is_err());
165 let t = coarsen(&a, &[("x", 3)], Reduction::Max, CoarsenBoundary::Trim).unwrap();
166 assert_eq!(t.shape(), vec![4, 1]);
167 }
168
169 #[test]
170 fn weighted_mean_ignores_null_weights() {
171 let a = NdArrowArray::try_new(
172 Arc::new(Float64Array::from(vec![Some(1.0), None, Some(3.0)])),
173 Dimensions::try_new([("lat", 3)]).unwrap(),
174 )
175 .unwrap();
176 let w = nd(vec![1.0, 5.0, 3.0], &[("lat", 3)]);
177 assert_eq!(f64s(&weighted_sum(&a, &w, None).unwrap()), vec![Some(10.0)]);
178 assert_eq!(f64s(&weighted_mean(&a, &w, None).unwrap()), vec![Some(2.5)]);
179 }
180
181 #[test]
182 fn dot_matches_matrix_product() {
183 let a = nd(vec![1.0, 2.0, 3.0, 4.0], &[("i", 2), ("k", 2)]);
184 let b = nd(vec![5.0, 6.0, 7.0, 8.0], &[("k", 2), ("j", 2)]);
185 let c = dot(&a, &b, &["k"]).unwrap();
186 assert_eq!(c.dims().names(), vec!["i", "j"]);
187 assert_eq!(
188 f64s(&c),
189 vec![Some(19.0), Some(22.0), Some(43.0), Some(50.0)]
190 );
191 assert!(dot(&a, &b, &["i"]).is_err());
192 }
193}