use kopitiam_core::{DType, Result};
use crate::storage::Storage;
use super::Tensor;
impl Tensor {
pub fn add(&self, other: &Tensor) -> Result<Tensor> {
broadcast_binary(self, other, |a, b| a + b)
}
pub fn sub(&self, other: &Tensor) -> Result<Tensor> {
broadcast_binary(self, other, |a, b| a - b)
}
pub fn mul(&self, other: &Tensor) -> Result<Tensor> {
broadcast_binary(self, other, |a, b| a * b)
}
pub fn div(&self, other: &Tensor) -> Result<Tensor> {
broadcast_binary(self, other, |a, b| a / b)
}
}
fn broadcast_binary(a: &Tensor, b: &Tensor, f: impl Fn(f32, f32) -> f32) -> Result<Tensor> {
a.require_dtype(DType::F32)?;
b.require_dtype(DType::F32)?;
let out_shape = a.shape.broadcast(&b.shape)?;
let a_view = a.broadcast_to(out_shape.clone())?;
let b_view = b.broadcast_to(out_shape.clone())?;
let Storage::F32(a_data) = a_view.storage.as_ref() else { unreachable!() };
let Storage::F32(b_data) = b_view.storage.as_ref() else { unreachable!() };
let result: Vec<f32> = a_view
.logical_offsets()
.zip(b_view.logical_offsets())
.map(|(ia, ib)| f(a_data[ia], b_data[ib]))
.collect();
Tensor::from_f32(result, out_shape)
}
#[cfg(test)]
mod tests {
use kopitiam_core::Error;
use super::*;
#[test]
fn add_same_shape_matches_hand_computation() {
let a = Tensor::from_f32(vec![1.0, 2.0, 3.0], [3]).unwrap();
let b = Tensor::from_f32(vec![10.0, 20.0, 30.0], [3]).unwrap();
assert_eq!(a.add(&b).unwrap().to_vec_f32().unwrap(), vec![11.0, 22.0, 33.0]);
}
#[test]
fn mul_broadcasts_a_row_vector_over_a_matrix() {
let a = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
let b = Tensor::from_f32(vec![10.0, 20.0, 30.0], [3]).unwrap();
let c = a.mul(&b).unwrap();
assert_eq!(c.shape().dims(), &[2, 3]);
assert_eq!(c.to_vec_f32().unwrap(), vec![10.0, 40.0, 90.0, 40.0, 100.0, 180.0]);
}
#[test]
fn sub_broadcasts_a_column_vector_over_a_matrix() {
let a = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [2, 2]).unwrap();
let b = Tensor::from_f32(vec![10.0, 20.0], [2, 1]).unwrap();
let c = a.sub(&b).unwrap();
assert_eq!(c.to_vec_f32().unwrap(), vec![-9.0, -8.0, -17.0, -16.0]);
}
#[test]
fn div_by_a_scalar_broadcasts_against_rank_zero() {
let a = Tensor::from_f32(vec![2.0, 4.0, 6.0], [3]).unwrap();
let scalar = Tensor::from_f32(vec![2.0], []).unwrap();
assert_eq!(a.div(&scalar).unwrap().to_vec_f32().unwrap(), vec![1.0, 2.0, 3.0]);
}
#[test]
fn broadcasting_incompatible_shapes_is_rejected() {
let a = Tensor::from_f32(vec![1.0, 2.0, 3.0], [3]).unwrap();
let b = Tensor::from_f32(vec![1.0, 2.0], [2]).unwrap();
assert!(matches!(a.add(&b), Err(Error::NotBroadcastable { .. })));
}
#[test]
fn elementwise_ops_reject_non_f32_operands() {
let a = Tensor::from_i32(vec![1, 2, 3], [3]).unwrap();
let b = Tensor::from_i32(vec![1, 2, 3], [3]).unwrap();
assert!(matches!(a.add(&b), Err(Error::DTypeMismatch { .. })));
}
}