use kopitiam_core::{DType, Error, Result, Shape};
use crate::storage::Storage;
use super::Tensor;
impl Tensor {
pub fn sum(&self, axis: usize, keepdim: bool) -> Result<Tensor> {
self.reduce(axis, keepdim, 0.0, |acc, v| acc + v)
}
pub fn max(&self, axis: usize, keepdim: bool) -> Result<Tensor> {
self.reduce(axis, keepdim, f32::NEG_INFINITY, f32::max)
}
pub fn argmax(&self, axis: usize, keepdim: bool) -> Result<Tensor> {
self.require_dtype(DType::F32)?;
let rank = self.rank();
if axis >= rank {
return Err(Error::IndexOutOfBounds { dim: axis, index: axis, len: rank });
}
let Storage::F32(data) = self.storage.as_ref() else { unreachable!() };
let contiguous: Vec<f32> = self.logical_offsets().map(|i| data[i]).collect();
let dims = self.shape.dims();
let axis_len = dims[axis];
let outer: usize = dims[..axis].iter().product();
let inner: usize = dims[axis + 1..].iter().product();
let mut out = vec![0i32; outer * inner];
for o in 0..outer {
for inn in 0..inner {
let mut best = f32::NEG_INFINITY;
let mut best_idx = 0usize;
for a in 0..axis_len {
let v = contiguous[o * axis_len * inner + a * inner + inn];
if v > best {
best = v;
best_idx = a;
}
}
out[o * inner + inn] = best_idx as i32;
}
}
let mut new_dims = dims.to_vec();
if keepdim {
new_dims[axis] = 1;
} else {
new_dims.remove(axis);
}
Tensor::from_i32(out, Shape::new(new_dims))
}
fn reduce(&self, axis: usize, keepdim: bool, init: f32, f: impl Fn(f32, f32) -> f32) -> Result<Tensor> {
self.require_dtype(DType::F32)?;
let rank = self.rank();
if axis >= rank {
return Err(Error::IndexOutOfBounds { dim: axis, index: axis, len: rank });
}
let Storage::F32(data) = self.storage.as_ref() else { unreachable!() };
let contiguous: Vec<f32> = self.logical_offsets().map(|i| data[i]).collect();
let dims = self.shape.dims();
let axis_len = dims[axis];
let outer: usize = dims[..axis].iter().product();
let inner: usize = dims[axis + 1..].iter().product();
let mut out = vec![init; outer * inner];
for o in 0..outer {
for inn in 0..inner {
let mut acc = init;
for a in 0..axis_len {
acc = f(acc, contiguous[o * axis_len * inner + a * inner + inn]);
}
out[o * inner + inn] = acc;
}
}
let mut new_dims = dims.to_vec();
if keepdim {
new_dims[axis] = 1;
} else {
new_dims.remove(axis);
}
Tensor::from_f32(out, Shape::new(new_dims))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sum_along_last_axis_matches_hand_computation() {
let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
let s = t.sum(1, false).unwrap();
assert_eq!(s.shape().dims(), &[2]);
assert_eq!(s.to_vec_f32().unwrap(), vec![6.0, 15.0]);
}
#[test]
fn sum_keepdim_preserves_rank() {
let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
let s = t.sum(1, true).unwrap();
assert_eq!(s.shape().dims(), &[2, 1]);
assert_eq!(s.to_vec_f32().unwrap(), vec![6.0, 15.0]);
}
#[test]
fn sum_along_axis_zero_matches_hand_computation() {
let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
let s = t.sum(0, false).unwrap();
assert_eq!(s.to_vec_f32().unwrap(), vec![5.0, 7.0, 9.0]);
}
#[test]
fn max_along_last_axis_matches_hand_computation() {
let t = Tensor::from_f32(vec![1.0, 5.0, 3.0, 9.0, 2.0, 4.0], [2, 3]).unwrap();
let m = t.max(1, false).unwrap();
assert_eq!(m.to_vec_f32().unwrap(), vec![5.0, 9.0]);
}
#[test]
fn reduce_rejects_an_axis_beyond_rank() {
let t = Tensor::from_f32(vec![1.0, 2.0], [2]).unwrap();
assert!(matches!(t.sum(1, false), Err(Error::IndexOutOfBounds { .. })));
}
#[test]
fn argmax_last_axis_matches_hand_computation_and_is_i32() {
let t = Tensor::from_f32(vec![1.0, 5.0, 3.0, 9.0, 2.0, 4.0], [2, 3]).unwrap();
let idx = t.argmax(1, false).unwrap();
assert_eq!(idx.dtype(), DType::I32);
assert_eq!(idx.shape().dims(), &[2]);
assert_eq!(idx.to_vec_i32().unwrap(), vec![1, 0]);
}
#[test]
fn argmax_keepdim_preserves_rank() {
let t = Tensor::from_f32(vec![1.0, 5.0, 3.0, 9.0, 2.0, 4.0], [2, 3]).unwrap();
let idx = t.argmax(1, true).unwrap();
assert_eq!(idx.shape().dims(), &[2, 1]);
assert_eq!(idx.to_vec_i32().unwrap(), vec![1, 0]);
}
#[test]
fn argmax_along_axis_zero_matches_hand_computation() {
let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
let idx = t.argmax(0, false).unwrap();
assert_eq!(idx.to_vec_i32().unwrap(), vec![1, 1, 1]);
}
#[test]
fn argmax_breaks_ties_towards_the_first_occurrence() {
let t = Tensor::from_f32(vec![7.0, 7.0, 7.0], [3]).unwrap();
let idx = t.argmax(0, false).unwrap();
assert_eq!(idx.to_vec_i32().unwrap(), vec![0]);
}
#[test]
fn argmax_picks_the_greedy_token_from_a_vocab_row() {
let logits = Tensor::from_f32(vec![0.1, -2.0, 0.5, 3.7, 1.2], [5]).unwrap();
let id = logits.argmax(0, false).unwrap();
assert_eq!(id.to_vec_i32().unwrap(), vec![3]);
}
#[test]
fn argmax_rejects_non_f32_input() {
let t = Tensor::from_i32(vec![1, 2, 3], [3]).unwrap();
assert!(matches!(t.argmax(0, false), Err(Error::DTypeMismatch { .. })));
}
#[test]
fn argmax_rejects_an_axis_beyond_rank() {
let t = Tensor::from_f32(vec![1.0, 2.0], [2]).unwrap();
assert!(matches!(t.argmax(1, false), Err(Error::IndexOutOfBounds { .. })));
}
#[test]
fn to_vec_i32_rejects_f32_input() {
let t = Tensor::from_f32(vec![1.0, 2.0], [2]).unwrap();
assert!(matches!(t.to_vec_i32(), Err(Error::DTypeMismatch { .. })));
}
}