use crate::array::Array;
use crate::error::{NumRs2Error, Result};
use crate::kernels::{borrow::operand, cast, reduce};
use num_traits::Float;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UfuncOp {
Add,
Subtract,
Multiply,
Divide,
Maximum,
Minimum,
}
impl UfuncOp {
pub fn name(self) -> &'static str {
match self {
UfuncOp::Add => "add",
UfuncOp::Subtract => "subtract",
UfuncOp::Multiply => "multiply",
UfuncOp::Divide => "divide",
UfuncOp::Maximum => "maximum",
UfuncOp::Minimum => "minimum",
}
}
pub fn identity<T: Float>(self) -> Option<T> {
match self {
UfuncOp::Add => Some(T::zero()),
UfuncOp::Multiply => Some(T::one()),
UfuncOp::Subtract | UfuncOp::Divide | UfuncOp::Maximum | UfuncOp::Minimum => None,
}
}
#[inline]
pub fn apply<T: Float>(self, a: T, b: T) -> T {
match self {
UfuncOp::Add => a + b,
UfuncOp::Subtract => a - b,
UfuncOp::Multiply => a * b,
UfuncOp::Divide => a / b,
UfuncOp::Maximum => {
if a.is_nan() || b.is_nan() {
T::nan()
} else if a > b {
a
} else {
b
}
}
UfuncOp::Minimum => {
if a.is_nan() || b.is_nan() {
T::nan()
} else if a < b {
a
} else {
b
}
}
}
}
}
fn normalize_axis(ax: isize, ndim: usize) -> Result<usize> {
let normalized = if ax < 0 { ax + ndim as isize } else { ax };
if normalized < 0 || normalized as usize >= ndim {
return Err(NumRs2Error::DimensionMismatch(format!(
"axis {ax} out of bounds for array of dimension {ndim}"
)));
}
Ok(normalized as usize)
}
fn unravel_index(mut flat: usize, shape: &[usize]) -> Vec<usize> {
let mut idx = vec![0usize; shape.len()];
for i in (0..shape.len()).rev() {
idx[i] = flat % shape[i];
flat /= shape[i];
}
idx
}
fn reduce_lane<T: Float>(
op: UfuncOp,
mut lane: impl Iterator<Item = T>,
initial: Option<T>,
) -> Result<T> {
match initial {
Some(seed) => Ok(lane.fold(seed, |acc, x| op.apply(acc, x))),
None => match lane.next() {
Some(first) => Ok(lane.fold(first, |acc, x| op.apply(acc, x))),
None => op.identity().ok_or_else(|| {
NumRs2Error::InvalidOperation(format!(
"zero-size array to reduction operation {} which has no identity",
op.name()
))
}),
},
}
}
fn reduce_full<T: Float + Clone + 'static>(
op: UfuncOp,
data: &[T],
initial: Option<T>,
) -> Result<T> {
match op {
UfuncOp::Add => {
if let Some(s) = cast::as_f64(data) {
let seed = initial.and_then(|v| v.to_f64()).unwrap_or(0.0);
return Ok(cast::f64_to(seed + reduce::sum_f64(s))
.expect("T == f64 per cast::as_f64 match"));
}
if let Some(s) = cast::as_f32(data) {
let seed = initial.and_then(|v| v.to_f32()).unwrap_or(0.0);
return Ok(cast::f32_to(seed + reduce::sum_f32(s))
.expect("T == f32 per cast::as_f32 match"));
}
}
UfuncOp::Multiply => {
if let Some(s) = cast::as_f64(data) {
let seed = initial.and_then(|v| v.to_f64()).unwrap_or(1.0);
return Ok(cast::f64_to(seed * reduce::prod_f64(s))
.expect("T == f64 per cast::as_f64 match"));
}
if let Some(s) = cast::as_f32(data) {
let seed = initial.and_then(|v| v.to_f32()).unwrap_or(1.0);
return Ok(cast::f32_to(seed * reduce::prod_f32(s))
.expect("T == f32 per cast::as_f32 match"));
}
}
UfuncOp::Subtract | UfuncOp::Divide | UfuncOp::Maximum | UfuncOp::Minimum => {}
}
reduce_lane(op, data.iter().copied(), initial)
}
fn wrap_full_result<T: Float + Clone>(value: T, ndim: usize, keepdims: bool) -> Result<Array<T>> {
if keepdims {
Array::from_vec_shape(vec![value], &vec![1usize; ndim])
} else {
Ok(Array::from_vec(vec![value]))
}
}
fn reduce_along_axis<T: Float + Clone>(
op: UfuncOp,
a: &Array<T>,
axis: usize,
keepdims: bool,
initial: Option<T>,
) -> Result<Array<T>> {
let shape = a.shape();
let axis_size = shape[axis];
let flat = operand(a);
let mut strides = vec![1usize; shape.len()];
for i in (0..shape.len().saturating_sub(1)).rev() {
strides[i] = strides[i + 1] * shape[i + 1];
}
let axis_stride = strides[axis];
let other_shape: Vec<usize> = shape
.iter()
.enumerate()
.filter(|&(i, _)| i != axis)
.map(|(_, &d)| d)
.collect();
let n_outer: usize = other_shape.iter().product();
let mut out_shape = shape.clone();
if keepdims {
out_shape[axis] = 1;
} else {
out_shape.remove(axis);
}
if out_shape.is_empty() {
out_shape.push(1);
}
let mut result = vec![T::zero(); n_outer];
for outer_idx in 0..n_outer {
let other_indices = unravel_index(outer_idx, &other_shape);
let mut base = 0usize;
let mut oi = 0usize;
for (i, &stride) in strides.iter().enumerate() {
if i == axis {
continue;
}
base += other_indices[oi] * stride;
oi += 1;
}
let lane = (0..axis_size).map(|k| flat[base + k * axis_stride]);
result[outer_idx] = reduce_lane(op, lane, initial)?;
}
Array::from_vec_shape(result, &out_shape)
}
pub fn ufunc_reduce<T>(
op: UfuncOp,
a: &Array<T>,
axis: Option<isize>,
keepdims: bool,
initial: Option<T>,
) -> Result<Array<T>>
where
T: Float + Clone + 'static,
{
match axis {
None => {
let flat = operand(a);
let value = reduce_full(op, &flat, initial)?;
wrap_full_result(value, a.ndim(), keepdims)
}
Some(ax) => {
let axis = normalize_axis(ax, a.ndim())?;
reduce_along_axis(op, a, axis, keepdims, initial)
}
}
}
fn accumulate_flat<T: Float + Clone>(op: UfuncOp, a: &Array<T>) -> Result<Array<T>> {
if a.is_empty() {
return Ok(a.clone());
}
let flat = operand(a);
let mut result = Vec::with_capacity(flat.len());
let mut acc = flat[0];
result.push(acc);
for &x in flat.iter().skip(1) {
acc = op.apply(acc, x);
result.push(acc);
}
Ok(Array::from_vec(result))
}
fn accumulate_along_axis<T: Float + Clone>(
op: UfuncOp,
a: &Array<T>,
axis: usize,
) -> Result<Array<T>> {
if a.is_empty() {
return Ok(a.clone());
}
let shape = a.shape();
let axis_size = shape[axis];
let flat = operand(a);
let mut strides = vec![1usize; shape.len()];
for i in (0..shape.len().saturating_sub(1)).rev() {
strides[i] = strides[i + 1] * shape[i + 1];
}
let axis_stride = strides[axis];
let other_shape: Vec<usize> = shape
.iter()
.enumerate()
.filter(|&(i, _)| i != axis)
.map(|(_, &d)| d)
.collect();
let n_outer: usize = other_shape.iter().product();
let mut result = vec![T::zero(); flat.len()];
for outer_idx in 0..n_outer {
let other_indices = unravel_index(outer_idx, &other_shape);
let mut base = 0usize;
let mut oi = 0usize;
for (i, &stride) in strides.iter().enumerate() {
if i == axis {
continue;
}
base += other_indices[oi] * stride;
oi += 1;
}
let mut acc = flat[base];
result[base] = acc;
for k in 1..axis_size {
let pos = base + k * axis_stride;
acc = op.apply(acc, flat[pos]);
result[pos] = acc;
}
}
Array::from_vec_shape(result, &shape)
}
pub fn ufunc_accumulate<T>(op: UfuncOp, a: &Array<T>, axis: Option<isize>) -> Result<Array<T>>
where
T: Float + Clone + Send + Sync + 'static,
{
match op {
UfuncOp::Add => crate::math::cumsum(a, axis, None),
UfuncOp::Multiply => crate::math::cumprod(a, axis, None),
UfuncOp::Subtract | UfuncOp::Divide | UfuncOp::Maximum | UfuncOp::Minimum => match axis {
None => accumulate_flat(op, a),
Some(ax) => {
let axis = normalize_axis(ax, a.ndim())?;
accumulate_along_axis(op, a, axis)
}
},
}
}
pub fn ufunc_outer<T>(op: UfuncOp, a: &Array<T>, b: &Array<T>) -> Result<Array<T>>
where
T: Float + Clone,
{
let a_flat = operand(a);
let b_flat = operand(b);
let mut result = Vec::with_capacity(a_flat.len() * b_flat.len());
for &av in a_flat.iter() {
for &bv in b_flat.iter() {
result.push(op.apply(av, bv));
}
}
let mut shape = a.shape();
shape.extend(b.shape());
Array::from_vec_shape(result, &shape)
}
fn reduceat_along_axis<T: Float + Clone>(
op: UfuncOp,
flat: &[T],
shape: &[usize],
axis: usize,
indices: &[usize],
) -> Result<Array<T>> {
let dim_size = shape[axis];
for &j in indices {
if j >= dim_size {
return Err(NumRs2Error::IndexOutOfBounds(format!(
"index {j} out of bounds for reduceat axis of size {dim_size}"
)));
}
}
let mut strides = vec![1usize; shape.len()];
for i in (0..shape.len().saturating_sub(1)).rev() {
strides[i] = strides[i + 1] * shape[i + 1];
}
let axis_stride = strides[axis];
let other_shape: Vec<usize> = shape
.iter()
.enumerate()
.filter(|&(i, _)| i != axis)
.map(|(_, &d)| d)
.collect();
let n_outer: usize = other_shape.iter().product();
let mut out_shape = shape.to_vec();
out_shape[axis] = indices.len();
let mut out_strides = vec![1usize; out_shape.len()];
for i in (0..out_shape.len().saturating_sub(1)).rev() {
out_strides[i] = out_strides[i + 1] * out_shape[i + 1];
}
let out_axis_stride = out_strides[axis];
let mut result = vec![T::zero(); n_outer * indices.len()];
for outer_idx in 0..n_outer {
let other_indices = unravel_index(outer_idx, &other_shape);
let mut in_base = 0usize;
let mut out_base = 0usize;
let mut oi = 0usize;
for i in 0..shape.len() {
if i == axis {
continue;
}
in_base += other_indices[oi] * strides[i];
out_base += other_indices[oi] * out_strides[i];
oi += 1;
}
for (out_k, &j) in indices.iter().enumerate() {
let end = if out_k + 1 < indices.len() {
indices[out_k + 1]
} else {
dim_size
};
let seg_end = if end > j { end } else { j + 1 };
let lane = (j..seg_end).map(|k| flat[in_base + k * axis_stride]);
result[out_base + out_k * out_axis_stride] = reduce_lane(op, lane, None)?;
}
}
Array::from_vec_shape(result, &out_shape)
}
pub fn ufunc_reduceat<T>(
op: UfuncOp,
a: &Array<T>,
indices: &[usize],
axis: Option<isize>,
) -> Result<Array<T>>
where
T: Float + Clone,
{
match axis {
None => {
let flat = operand(a);
reduceat_along_axis(op, &flat, &[flat.len()], 0, indices)
}
Some(ax) => {
let axis = normalize_axis(ax, a.ndim())?;
let flat = operand(a);
reduceat_along_axis(op, &flat, &a.shape(), axis, indices)
}
}
}
pub fn ufunc_at<T>(op: UfuncOp, a: &mut Array<T>, indices: &[usize], b: &Array<T>) -> Result<()>
where
T: Float + Clone,
{
let a_shape = a.shape();
if a_shape.is_empty() {
return Err(NumRs2Error::InvalidOperation(
"ufunc_at requires an array with at least 1 dimension".to_string(),
));
}
let n0 = a_shape[0];
let rest_shape = a_shape[1..].to_vec();
let mut expected_b_shape = vec![indices.len()];
expected_b_shape.extend_from_slice(&rest_shape);
if b.shape() != expected_b_shape {
return Err(NumRs2Error::ShapeMismatch {
expected: expected_b_shape,
actual: b.shape(),
});
}
for &idx in indices {
if idx >= n0 {
return Err(NumRs2Error::IndexOutOfBounds(format!(
"index {idx} out of bounds for axis 0 with size {n0}"
)));
}
}
let row_len: usize = rest_shape.iter().product();
if row_len == 0 {
return Ok(());
}
let b_flat = operand(b);
let nd = a.array_mut();
let mut multi_index = vec![0usize; a_shape.len()];
for (k, &row) in indices.iter().enumerate() {
multi_index[0] = row;
for r in 0..row_len {
let rest_idx = unravel_index(r, &rest_shape);
multi_index[1..].copy_from_slice(&rest_idx);
let b_val = b_flat[k * row_len + r];
match nd.get_mut(multi_index.as_slice()) {
Some(elem) => *elem = op.apply(*elem, b_val),
None => return Err(NumRs2Error::bulk_index_oob(&multi_index)),
}
}
}
Ok(())
}
pub fn ufunc_where<T>(
op: UfuncOp,
a: &Array<T>,
b: &Array<T>,
mask: &Array<bool>,
) -> Result<Array<T>>
where
T: Float + Clone,
{
let a_shape = a.shape();
if b.shape() != a_shape {
return Err(NumRs2Error::ShapeMismatch {
expected: a_shape,
actual: b.shape(),
});
}
if mask.shape() != a_shape {
return Err(NumRs2Error::ShapeMismatch {
expected: a_shape,
actual: mask.shape(),
});
}
let a_flat = operand(a);
let b_flat = operand(b);
let mask_flat = operand(mask);
let result: Vec<T> = a_flat
.iter()
.zip(b_flat.iter())
.zip(mask_flat.iter())
.map(|((&av, &bv), &m)| if m { op.apply(av, bv) } else { av })
.collect();
Array::from_vec_shape(result, &a_shape)
}
pub fn add_where<T: Float + Clone>(
a: &Array<T>,
b: &Array<T>,
mask: &Array<bool>,
) -> Result<Array<T>> {
ufunc_where(UfuncOp::Add, a, b, mask)
}
pub fn subtract_where<T: Float + Clone>(
a: &Array<T>,
b: &Array<T>,
mask: &Array<bool>,
) -> Result<Array<T>> {
ufunc_where(UfuncOp::Subtract, a, b, mask)
}
pub fn multiply_where<T: Float + Clone>(
a: &Array<T>,
b: &Array<T>,
mask: &Array<bool>,
) -> Result<Array<T>> {
ufunc_where(UfuncOp::Multiply, a, b, mask)
}
pub fn divide_where<T: Float + Clone>(
a: &Array<T>,
b: &Array<T>,
mask: &Array<bool>,
) -> Result<Array<T>> {
ufunc_where(UfuncOp::Divide, a, b, mask)
}
#[cfg(test)]
mod tests {
use super::*;
fn arr(v: Vec<f64>) -> Array<f64> {
Array::from_vec(v)
}
#[test]
fn maximum_minimum_apply_propagate_nan_symmetrically() {
assert!(UfuncOp::Maximum.apply(f64::NAN, 5.0).is_nan());
assert!(UfuncOp::Maximum.apply(5.0, f64::NAN).is_nan());
assert!(UfuncOp::Minimum.apply(f64::NAN, 5.0).is_nan());
assert!(UfuncOp::Minimum.apply(5.0, f64::NAN).is_nan());
assert_eq!(UfuncOp::Maximum.apply(2.0, 5.0), 5.0);
assert_eq!(UfuncOp::Minimum.apply(2.0, 5.0), 2.0);
}
#[test]
fn identity_matches_numpy() {
assert_eq!(UfuncOp::Add.identity::<f64>(), Some(0.0));
assert_eq!(UfuncOp::Multiply.identity::<f64>(), Some(1.0));
assert_eq!(UfuncOp::Maximum.identity::<f64>(), None);
assert_eq!(UfuncOp::Minimum.identity::<f64>(), None);
assert_eq!(UfuncOp::Subtract.identity::<f64>(), None);
assert_eq!(UfuncOp::Divide.identity::<f64>(), None);
}
fn a_2x3() -> Array<f64> {
Array::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).reshape(&[2, 3])
}
#[test]
fn reduce_add_axis_none_matches_numpy() {
let a = a_2x3();
let r = ufunc_reduce(UfuncOp::Add, &a, None, false, None).expect("reduce should succeed");
assert_eq!(r.shape(), vec![1]);
assert_eq!(r.to_vec(), vec![21.0]);
}
#[test]
fn reduce_add_axis_none_keepdims_matches_numpy_value_this_crate_shape() {
let a = a_2x3();
let r = ufunc_reduce(UfuncOp::Add, &a, None, true, None).expect("reduce should succeed");
assert_eq!(r.shape(), vec![1, 1]);
assert_eq!(r.to_vec(), vec![21.0]);
}
#[test]
fn reduce_add_axis0_and_axis1_match_numpy() {
let a = a_2x3();
let r0 = ufunc_reduce(UfuncOp::Add, &a, Some(0), false, None).expect("reduce ok");
assert_eq!(r0.to_vec(), vec![5.0, 7.0, 9.0]);
let r1 = ufunc_reduce(UfuncOp::Add, &a, Some(1), false, None).expect("reduce ok");
assert_eq!(r1.to_vec(), vec![6.0, 15.0]);
}
#[test]
fn reduce_negative_axis_matches_positive_equivalent() {
let a = a_2x3();
let r_neg = ufunc_reduce(UfuncOp::Add, &a, Some(-1), false, None).expect("reduce ok");
let r_pos = ufunc_reduce(UfuncOp::Add, &a, Some(1), false, None).expect("reduce ok");
assert_eq!(r_neg.to_vec(), r_pos.to_vec());
assert_eq!(r_neg.to_vec(), vec![6.0, 15.0]);
}
#[test]
fn reduce_keepdims_matches_numpy() {
let a = a_2x3();
let r = ufunc_reduce(UfuncOp::Add, &a, Some(1), true, None).expect("reduce ok");
assert_eq!(r.shape(), vec![2, 1]);
assert_eq!(r.to_vec(), vec![6.0, 15.0]);
}
#[test]
fn reduce_multiply_and_extrema_match_numpy() {
let a = a_2x3();
let mul = ufunc_reduce(UfuncOp::Multiply, &a, Some(1), false, None).expect("ok");
assert_eq!(mul.to_vec(), vec![6.0, 120.0]);
let mx = ufunc_reduce(UfuncOp::Maximum, &a, Some(0), false, None).expect("ok");
assert_eq!(mx.to_vec(), vec![4.0, 5.0, 6.0]);
let mn = ufunc_reduce(UfuncOp::Minimum, &a, Some(1), false, None).expect("ok");
assert_eq!(mn.to_vec(), vec![1.0, 4.0]);
}
#[test]
fn reduce_with_initial_matches_numpy() {
let a = a_2x3();
let add_i = ufunc_reduce(UfuncOp::Add, &a, Some(1), false, Some(100.0)).expect("ok");
assert_eq!(add_i.to_vec(), vec![106.0, 115.0]);
let mul_i = ufunc_reduce(UfuncOp::Multiply, &a, Some(1), false, Some(2.0)).expect("ok");
assert_eq!(mul_i.to_vec(), vec![12.0, 240.0]);
let max_i0 = ufunc_reduce(UfuncOp::Maximum, &a, Some(1), false, Some(0.0)).expect("ok");
assert_eq!(max_i0.to_vec(), vec![3.0, 6.0]);
let max_i100 = ufunc_reduce(UfuncOp::Maximum, &a, Some(1), false, Some(100.0)).expect("ok");
assert_eq!(max_i100.to_vec(), vec![100.0, 100.0]);
}
#[test]
fn reduce_empty_array_matches_numpy() {
let empty = arr(vec![]);
assert_eq!(
ufunc_reduce(UfuncOp::Add, &empty, None, false, None)
.expect("ok")
.to_vec(),
vec![0.0]
);
assert_eq!(
ufunc_reduce(UfuncOp::Multiply, &empty, None, false, None)
.expect("ok")
.to_vec(),
vec![1.0]
);
assert!(ufunc_reduce(UfuncOp::Maximum, &empty, None, false, None).is_err());
assert!(ufunc_reduce(UfuncOp::Minimum, &empty, None, false, None).is_err());
assert_eq!(
ufunc_reduce(UfuncOp::Add, &empty, None, false, Some(5.0))
.expect("ok")
.to_vec(),
vec![5.0]
);
assert_eq!(
ufunc_reduce(UfuncOp::Maximum, &empty, None, false, Some(5.0))
.expect("ok")
.to_vec(),
vec![5.0]
);
}
#[test]
fn reduce_empty_axis_matches_numpy() {
let b: Array<f64> = Array::from_vec(vec![]).reshape(&[3, 0]);
let r = ufunc_reduce(UfuncOp::Add, &b, Some(1), false, None).expect("ok");
assert_eq!(r.to_vec(), vec![0.0, 0.0, 0.0]);
assert!(ufunc_reduce(UfuncOp::Maximum, &b, Some(1), false, None).is_err());
let r_init = ufunc_reduce(UfuncOp::Add, &b, Some(1), false, Some(7.0)).expect("ok");
assert_eq!(r_init.to_vec(), vec![7.0, 7.0, 7.0]);
let max_init = ufunc_reduce(UfuncOp::Maximum, &b, Some(1), false, Some(7.0)).expect("ok");
assert_eq!(max_init.to_vec(), vec![7.0, 7.0, 7.0]);
let r0 = ufunc_reduce(UfuncOp::Add, &b, Some(0), false, None).expect("ok");
assert_eq!(r0.shape(), vec![0]);
let r1_keep = ufunc_reduce(UfuncOp::Add, &b, Some(1), true, None).expect("ok");
assert_eq!(r1_keep.shape(), vec![3, 1]);
}
#[test]
fn reduce_1d_empty_axis_keepdims_matches_numpy() {
let empty_1d: Array<f64> = Array::from_vec(vec![]).reshape(&[0]);
let r = ufunc_reduce(UfuncOp::Add, &empty_1d, Some(0), true, None).expect("ok");
assert_eq!(r.shape(), vec![1]);
assert_eq!(r.to_vec(), vec![0.0]);
}
#[test]
fn reduce_3d_negative_axis_matches_numpy() {
let a: Array<f64> =
Array::from_vec((0..24).map(|i| i as f64).collect()).reshape(&[2, 3, 4]);
let r = ufunc_reduce(UfuncOp::Add, &a, Some(-2), false, None).expect("ok");
assert_eq!(r.shape(), vec![2, 4]);
assert_eq!(
r.to_vec(),
vec![12.0, 15.0, 18.0, 21.0, 48.0, 51.0, 54.0, 57.0]
);
let r2 = ufunc_reduce(UfuncOp::Maximum, &a, Some(-1), true, None).expect("ok");
assert_eq!(r2.shape(), vec![2, 3, 1]);
assert_eq!(r2.to_vec(), vec![3.0, 7.0, 11.0, 15.0, 19.0, 23.0]);
}
#[test]
fn reduce_out_of_bounds_axis_errors() {
let a = a_2x3();
assert!(ufunc_reduce(UfuncOp::Add, &a, Some(2), false, None).is_err());
assert!(ufunc_reduce(UfuncOp::Add, &a, Some(-3), false, None).is_err());
}
#[test]
fn reduce_f32_fast_path_matches_f64_generic_path() {
let data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let a32 = Array::from_vec(data.clone());
let r32 = ufunc_reduce(UfuncOp::Add, &a32, None, false, None).expect("ok");
assert_eq!(r32.to_vec(), vec![15.0f32]);
let mul32 = ufunc_reduce(UfuncOp::Multiply, &a32, None, false, None).expect("ok");
assert_eq!(mul32.to_vec(), vec![120.0f32]);
}
#[test]
fn accumulate_add_multiply_delegate_correctly() {
let a = arr(vec![1.0, 2.0, 3.0, 4.0]);
let add_acc = ufunc_accumulate(UfuncOp::Add, &a, None).expect("ok");
assert_eq!(add_acc.to_vec(), vec![1.0, 3.0, 6.0, 10.0]);
let mul_acc = ufunc_accumulate(UfuncOp::Multiply, &a, None).expect("ok");
assert_eq!(mul_acc.to_vec(), vec![1.0, 2.0, 6.0, 24.0]);
}
#[test]
fn accumulate_maximum_minimum_match_numpy() {
let a = arr(vec![1.0, 3.0, 2.0, 5.0, 4.0]);
let mx = ufunc_accumulate(UfuncOp::Maximum, &a, None).expect("ok");
assert_eq!(mx.to_vec(), vec![1.0, 3.0, 3.0, 5.0, 5.0]);
let b = arr(vec![5.0, 3.0, 4.0, 1.0, 2.0]);
let mn = ufunc_accumulate(UfuncOp::Minimum, &b, None).expect("ok");
assert_eq!(mn.to_vec(), vec![5.0, 3.0, 3.0, 1.0, 1.0]);
}
#[test]
fn accumulate_subtract_divide_match_numpy() {
let a = arr(vec![10.0, 1.0, 2.0, 3.0]);
let sub = ufunc_accumulate(UfuncOp::Subtract, &a, None).expect("ok");
assert_eq!(sub.to_vec(), vec![10.0, 9.0, 7.0, 4.0]);
let div = ufunc_accumulate(UfuncOp::Divide, &a, None).expect("ok");
let got = div.to_vec();
let want = [10.0, 10.0, 5.0, 5.0 / 3.0];
for (g, w) in got.iter().zip(want.iter()) {
assert!((g - w).abs() < 1e-9, "got {g}, want {w}");
}
}
#[test]
fn accumulate_with_axis_matches_numpy() {
let m = Array::from_vec(vec![1.0, 5.0, 2.0, 8.0, 1.0, 9.0]).reshape(&[2, 3]);
let mx = ufunc_accumulate(UfuncOp::Maximum, &m, Some(1)).expect("ok");
assert_eq!(mx.to_vec(), vec![1.0, 5.0, 5.0, 8.0, 8.0, 9.0]);
let mn = ufunc_accumulate(UfuncOp::Minimum, &m, Some(0)).expect("ok");
assert_eq!(mn.to_vec(), vec![1.0, 5.0, 2.0, 1.0, 1.0, 2.0]);
}
#[test]
fn accumulate_negative_axis_matches_positive() {
let m = Array::from_vec(vec![1.0, 5.0, 2.0, 8.0, 1.0, 9.0]).reshape(&[2, 3]);
let neg = ufunc_accumulate(UfuncOp::Maximum, &m, Some(-1)).expect("ok");
let pos = ufunc_accumulate(UfuncOp::Maximum, &m, Some(1)).expect("ok");
assert_eq!(neg.to_vec(), pos.to_vec());
}
#[test]
fn outer_add_multiply_match_numpy() {
let a = arr(vec![1.0, 2.0, 3.0]);
let b = arr(vec![10.0, 20.0]);
let add_out = ufunc_outer(UfuncOp::Add, &a, &b).expect("ok");
assert_eq!(add_out.shape(), vec![3, 2]);
assert_eq!(add_out.to_vec(), vec![11.0, 21.0, 12.0, 22.0, 13.0, 23.0]);
let mul_out = ufunc_outer(UfuncOp::Multiply, &a, &b).expect("ok");
assert_eq!(mul_out.to_vec(), vec![10.0, 20.0, 20.0, 40.0, 30.0, 60.0]);
}
#[test]
fn outer_maximum_matches_numpy() {
let a = arr(vec![1.0, 5.0, 2.0]);
let b = arr(vec![3.0, 1.0]);
let out = ufunc_outer(UfuncOp::Maximum, &a, &b).expect("ok");
assert_eq!(out.to_vec(), vec![3.0, 1.0, 5.0, 5.0, 3.0, 2.0]);
}
#[test]
fn outer_full_nd_shape_matches_numpy() {
let a: Array<f64> = Array::from_vec((1..7).map(|i| i as f64).collect()).reshape(&[2, 3]);
let b = arr(vec![1.0, 2.0]);
let out = ufunc_outer(UfuncOp::Add, &a, &b).expect("ok");
assert_eq!(out.shape(), vec![2, 3, 2]);
assert_eq!(
out.to_vec(),
vec![2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 6.0, 6.0, 7.0, 7.0, 8.0]
);
}
fn seq10() -> Array<f64> {
arr((0..10).map(|i| i as f64).collect())
}
#[test]
fn reduceat_basic_matches_numpy() {
let a = seq10();
let out = ufunc_reduceat(UfuncOp::Add, &a, &[0, 4, 7], None).expect("ok");
assert_eq!(out.to_vec(), vec![6.0, 15.0, 24.0]);
}
#[test]
fn reduceat_single_element_rule_matches_numpy() {
let a = seq10();
let out1 = ufunc_reduceat(UfuncOp::Add, &a, &[0, 4, 4, 7], None).expect("ok");
assert_eq!(out1.to_vec(), vec![6.0, 4.0, 15.0, 24.0]);
let out2 = ufunc_reduceat(UfuncOp::Add, &a, &[0, 2, 2, 2, 7], None).expect("ok");
assert_eq!(out2.to_vec(), vec![1.0, 2.0, 2.0, 20.0, 24.0]);
}
#[test]
fn reduceat_last_index_runs_to_end() {
let a = seq10();
let out = ufunc_reduceat(UfuncOp::Add, &a, &[9], None).expect("ok");
assert_eq!(out.to_vec(), vec![9.0]);
}
#[test]
fn reduceat_empty_indices_yields_empty_result() {
let a = seq10();
let out =
ufunc_reduceat(UfuncOp::Add, &a, &[], None).expect("empty indices should succeed");
assert_eq!(out.shape(), vec![0]);
assert!(out.to_vec().is_empty());
let empty: Array<f64> = arr(vec![]);
let out2 =
ufunc_reduceat(UfuncOp::Add, &empty, &[], None).expect("empty indices should succeed");
assert_eq!(out2.shape(), vec![0]);
}
#[test]
fn reduceat_out_of_bounds_index_errors() {
let a = seq10();
assert!(ufunc_reduceat(UfuncOp::Add, &a, &[10], None).is_err());
}
#[test]
fn reduceat_with_axis_matches_numpy() {
let a3: Array<f64> = Array::from_vec((1..13).map(|i| i as f64).collect()).reshape(&[3, 4]);
let out = ufunc_reduceat(UfuncOp::Add, &a3, &[0, 2], Some(1)).expect("ok");
assert_eq!(out.shape(), vec![3, 2]);
assert_eq!(out.to_vec(), vec![3.0, 7.0, 11.0, 15.0, 19.0, 23.0]);
let mul = ufunc_reduceat(UfuncOp::Multiply, &a3, &[0, 1, 3], Some(1)).expect("ok");
assert_eq!(
mul.to_vec(),
vec![1.0, 6.0, 4.0, 5.0, 42.0, 8.0, 9.0, 110.0, 12.0]
);
let axis0 = ufunc_reduceat(UfuncOp::Add, &a3, &[0, 2], Some(0)).expect("ok");
assert_eq!(axis0.shape(), vec![2, 4]);
assert_eq!(
axis0.to_vec(),
vec![6.0, 8.0, 10.0, 12.0, 9.0, 10.0, 11.0, 12.0]
);
let neg = ufunc_reduceat(UfuncOp::Add, &a3, &[0, 2], Some(-1)).expect("ok");
assert_eq!(neg.to_vec(), out.to_vec());
}
#[test]
fn at_add_repeated_indices_accumulates() {
let mut a = arr(vec![1.0, 2.0, 3.0]);
let b = arr(vec![10.0, 20.0, 30.0]);
ufunc_at(UfuncOp::Add, &mut a, &[0, 0, 1], &b).expect("at should succeed");
assert_eq!(a.to_vec(), vec![31.0, 32.0, 3.0]);
}
#[test]
fn at_multiply_repeated_indices_accumulates() {
let mut a = arr(vec![1.0, 2.0, 3.0, 4.0]);
let b = arr(vec![2.0, 3.0, 5.0]);
ufunc_at(UfuncOp::Multiply, &mut a, &[0, 0, 3], &b).expect("at should succeed");
assert_eq!(a.to_vec(), vec![6.0, 2.0, 3.0, 20.0]);
}
#[test]
fn at_triple_repeat_at_same_index() {
let mut a = arr(vec![0.0, 0.0, 0.0, 0.0, 0.0]);
let b = arr(vec![1.0, 1.0, 1.0, 1.0, 1.0]);
ufunc_at(UfuncOp::Add, &mut a, &[0, 0, 0, 1, 1], &b).expect("at should succeed");
assert_eq!(a.to_vec(), vec![3.0, 2.0, 0.0, 0.0, 0.0]);
}
#[test]
fn at_nd_repeated_indices_matches_numpy() {
let mut a4: Array<f64> = Array::from_vec(vec![0.0; 6]).reshape(&[3, 2]);
let b4 = Array::from_vec(vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]).reshape(&[3, 2]);
ufunc_at(UfuncOp::Add, &mut a4, &[0, 0, 1], &b4).expect("at should succeed");
assert_eq!(a4.to_vec(), vec![3.0, 3.0, 3.0, 3.0, 0.0, 0.0]);
}
#[test]
fn at_out_of_bounds_index_errors() {
let mut a = arr(vec![0.0, 0.0, 0.0]);
let b = arr(vec![1.0]);
assert!(ufunc_at(UfuncOp::Add, &mut a, &[5], &b).is_err());
}
#[test]
fn at_shape_mismatch_errors() {
let mut a = arr(vec![0.0, 0.0, 0.0]);
let b = arr(vec![1.0, 2.0]); assert!(ufunc_at(UfuncOp::Add, &mut a, &[0, 1, 2], &b).is_err());
}
#[test]
fn where_add_subtract_multiply_divide_match_numpy_out_eq_a() {
let a = arr(vec![1.0, 2.0, 3.0, 4.0]);
let b = arr(vec![10.0, 20.0, 30.0, 40.0]);
let mask = Array::from_vec(vec![true, false, true, false]);
assert_eq!(
add_where(&a, &b, &mask).expect("ok").to_vec(),
vec![11.0, 2.0, 33.0, 4.0]
);
assert_eq!(
subtract_where(&a, &b, &mask).expect("ok").to_vec(),
vec![-9.0, 2.0, -27.0, 4.0]
);
assert_eq!(
multiply_where(&a, &b, &mask).expect("ok").to_vec(),
vec![10.0, 2.0, 90.0, 4.0]
);
assert_eq!(
divide_where(&a, &b, &mask).expect("ok").to_vec(),
vec![0.1, 2.0, 0.1, 4.0]
);
}
#[test]
fn where_generic_dispatch_matches_named_wrappers() {
let a = arr(vec![1.0, 2.0]);
let b = arr(vec![5.0, 6.0]);
let mask = Array::from_vec(vec![true, true]);
assert_eq!(
ufunc_where(UfuncOp::Add, &a, &b, &mask)
.expect("ok")
.to_vec(),
add_where(&a, &b, &mask).expect("ok").to_vec()
);
}
#[test]
fn where_shape_mismatch_errors_for_b_and_for_mask() {
let a = arr(vec![1.0, 2.0, 3.0]);
let b_wrong = arr(vec![1.0, 2.0]);
let mask_ok = Array::from_vec(vec![true, false, true]);
let mask_wrong = Array::from_vec(vec![true, false]);
let b_ok = arr(vec![1.0, 2.0, 3.0]);
assert!(ufunc_where(UfuncOp::Add, &a, &b_wrong, &mask_ok).is_err());
assert!(ufunc_where(UfuncOp::Add, &a, &b_ok, &mask_wrong).is_err());
}
}