use crate::mat::Mat;
use crate::mat_ref::MatRef;
use core::marker::PhantomData;
use num_complex::Complex;
use num_traits::Zero;
use oxiblas_core::scalar::Scalar;
pub trait Expr: Sized {
type Elem: Scalar + bytemuck::Zeroable + Zero;
fn nrows(&self) -> usize;
fn ncols(&self) -> usize;
fn shape(&self) -> (usize, usize) {
(self.nrows(), self.ncols())
}
fn eval_elem(&self, row: usize, col: usize) -> Self::Elem;
fn eval(&self) -> Mat<Self::Elem> {
let mut result = Mat::zeros(self.nrows(), self.ncols());
self.eval_into(&mut result);
result
}
fn eval_into(&self, target: &mut Mat<Self::Elem>) {
assert_eq!(
target.shape(),
self.shape(),
"eval_into: target shape must match expression shape"
);
let (nrows, ncols) = self.shape();
for col in 0..ncols {
for row in 0..nrows {
target[(row, col)] = self.eval_elem(row, col);
}
}
}
fn t(self) -> ExprTranspose<Self> {
ExprTranspose { inner: self }
}
fn scale(self, alpha: Self::Elem) -> ExprScale<Self> {
ExprScale { inner: self, alpha }
}
fn add<E: Expr<Elem = Self::Elem>>(self, other: E) -> ExprAdd<Self, E> {
assert_eq!(
self.shape(),
other.shape(),
"Matrix dimensions must match for addition"
);
ExprAdd {
lhs: self,
rhs: other,
}
}
fn sub<E: Expr<Elem = Self::Elem>>(self, other: E) -> ExprSub<Self, E> {
assert_eq!(
self.shape(),
other.shape(),
"Matrix dimensions must match for subtraction"
);
ExprSub {
lhs: self,
rhs: other,
}
}
fn neg(self) -> ExprNeg<Self> {
ExprNeg { inner: self }
}
fn matmul<E: Expr<Elem = Self::Elem>>(self, other: E) -> ExprMul<Self, E> {
assert_eq!(
self.ncols(),
other.nrows(),
"Matrix dimensions must be compatible for multiplication"
);
ExprMul {
lhs: self,
rhs: other,
}
}
}
pub trait ComplexExpr: Expr
where
Self::Elem: ComplexScalar,
{
fn conj(self) -> ExprConj<Self> {
ExprConj { inner: self }
}
fn h(self) -> ExprHermitian<Self> {
ExprHermitian { inner: self }
}
}
impl<E: Expr> ComplexExpr for E where E::Elem: ComplexScalar {}
pub trait ComplexScalar: Scalar + bytemuck::Zeroable + Zero {
fn conj(&self) -> Self;
}
impl ComplexScalar for Complex<f32> {
fn conj(&self) -> Self {
Complex::conj(self)
}
}
impl ComplexScalar for Complex<f64> {
fn conj(&self) -> Self {
Complex::conj(self)
}
}
impl ComplexScalar for f32 {
fn conj(&self) -> Self {
*self
}
}
impl ComplexScalar for f64 {
fn conj(&self) -> Self {
*self
}
}
#[derive(Clone, Copy)]
pub struct ExprLeaf<'a, T: Scalar + bytemuck::Zeroable + Zero> {
mat: MatRef<'a, T>,
}
impl<'a, T: Scalar + bytemuck::Zeroable + Zero> ExprLeaf<'a, T> {
pub fn new(mat: MatRef<'a, T>) -> Self {
Self { mat }
}
}
impl<'a, T: Scalar + bytemuck::Zeroable + Zero> Expr for ExprLeaf<'a, T> {
type Elem = T;
fn nrows(&self) -> usize {
self.mat.nrows()
}
fn ncols(&self) -> usize {
self.mat.ncols()
}
fn eval_elem(&self, row: usize, col: usize) -> T {
self.mat[(row, col)]
}
}
pub trait LazyExt<'a, T: Scalar + bytemuck::Zeroable + Zero> {
fn lazy(self) -> ExprLeaf<'a, T>;
}
impl<'a, T: Scalar + bytemuck::Zeroable + Zero> LazyExt<'a, T> for MatRef<'a, T> {
fn lazy(self) -> ExprLeaf<'a, T> {
ExprLeaf::new(self)
}
}
pub struct ExprTranspose<E: Expr> {
inner: E,
}
impl<E: Expr> Expr for ExprTranspose<E> {
type Elem = E::Elem;
fn nrows(&self) -> usize {
self.inner.ncols()
}
fn ncols(&self) -> usize {
self.inner.nrows()
}
fn eval_elem(&self, row: usize, col: usize) -> Self::Elem {
self.inner.eval_elem(col, row)
}
}
impl<E: Expr> ExprTranspose<ExprTranspose<E>> {
pub fn simplify(self) -> E {
self.inner.inner
}
}
pub struct ExprScale<E: Expr> {
inner: E,
alpha: E::Elem,
}
impl<E: Expr> Expr for ExprScale<E> {
type Elem = E::Elem;
fn nrows(&self) -> usize {
self.inner.nrows()
}
fn ncols(&self) -> usize {
self.inner.ncols()
}
fn eval_elem(&self, row: usize, col: usize) -> Self::Elem {
self.inner.eval_elem(row, col) * self.alpha
}
}
impl<E: Expr> ExprScale<ExprScale<E>> {
pub fn simplify(self) -> ExprScale<E> {
ExprScale {
inner: self.inner.inner,
alpha: self.alpha * self.inner.alpha,
}
}
}
pub struct ExprNeg<E: Expr> {
inner: E,
}
impl<E: Expr> Expr for ExprNeg<E> {
type Elem = E::Elem;
fn nrows(&self) -> usize {
self.inner.nrows()
}
fn ncols(&self) -> usize {
self.inner.ncols()
}
fn eval_elem(&self, row: usize, col: usize) -> Self::Elem {
Self::Elem::zero() - self.inner.eval_elem(row, col)
}
}
impl<E: Expr> ExprNeg<ExprNeg<E>> {
pub fn simplify(self) -> E {
self.inner.inner
}
}
pub struct ExprAdd<L: Expr, R: Expr<Elem = L::Elem>> {
lhs: L,
rhs: R,
}
impl<L: Expr, R: Expr<Elem = L::Elem>> Expr for ExprAdd<L, R> {
type Elem = L::Elem;
fn nrows(&self) -> usize {
self.lhs.nrows()
}
fn ncols(&self) -> usize {
self.lhs.ncols()
}
fn eval_elem(&self, row: usize, col: usize) -> Self::Elem {
self.lhs.eval_elem(row, col) + self.rhs.eval_elem(row, col)
}
}
pub struct ExprSub<L: Expr, R: Expr<Elem = L::Elem>> {
lhs: L,
rhs: R,
}
impl<L: Expr, R: Expr<Elem = L::Elem>> Expr for ExprSub<L, R> {
type Elem = L::Elem;
fn nrows(&self) -> usize {
self.lhs.nrows()
}
fn ncols(&self) -> usize {
self.lhs.ncols()
}
fn eval_elem(&self, row: usize, col: usize) -> Self::Elem {
self.lhs.eval_elem(row, col) - self.rhs.eval_elem(row, col)
}
}
pub struct ExprMul<L: Expr, R: Expr<Elem = L::Elem>> {
lhs: L,
rhs: R,
}
impl<L: Expr, R: Expr<Elem = L::Elem>> Expr for ExprMul<L, R> {
type Elem = L::Elem;
fn nrows(&self) -> usize {
self.lhs.nrows()
}
fn ncols(&self) -> usize {
self.rhs.ncols()
}
fn eval_elem(&self, row: usize, col: usize) -> Self::Elem {
let k = self.lhs.ncols();
let mut sum = Self::Elem::zero();
for kk in 0..k {
sum += self.lhs.eval_elem(row, kk) * self.rhs.eval_elem(kk, col);
}
sum
}
fn eval_into(&self, target: &mut Mat<Self::Elem>) {
assert_eq!(
target.shape(),
self.shape(),
"eval_into: target shape must match expression shape"
);
let lhs = self.lhs.eval();
let rhs = self.rhs.eval();
let k = self.lhs.ncols();
let (nrows, ncols) = self.shape();
for col in 0..ncols {
for row in 0..nrows {
let mut sum = Self::Elem::zero();
for kk in 0..k {
sum += lhs[(row, kk)] * rhs[(kk, col)];
}
target[(row, col)] = sum;
}
}
}
}
pub struct ExprConj<E: Expr>
where
E::Elem: ComplexScalar,
{
inner: E,
}
impl<E: Expr> Expr for ExprConj<E>
where
E::Elem: ComplexScalar,
{
type Elem = E::Elem;
fn nrows(&self) -> usize {
self.inner.nrows()
}
fn ncols(&self) -> usize {
self.inner.ncols()
}
fn eval_elem(&self, row: usize, col: usize) -> Self::Elem {
ComplexScalar::conj(&self.inner.eval_elem(row, col))
}
}
impl<E: Expr> ExprConj<ExprConj<E>>
where
E::Elem: ComplexScalar,
{
pub fn simplify(self) -> E {
self.inner.inner
}
}
pub struct ExprHermitian<E: Expr>
where
E::Elem: ComplexScalar,
{
inner: E,
}
impl<E: Expr> Expr for ExprHermitian<E>
where
E::Elem: ComplexScalar,
{
type Elem = E::Elem;
fn nrows(&self) -> usize {
self.inner.ncols()
}
fn ncols(&self) -> usize {
self.inner.nrows()
}
fn eval_elem(&self, row: usize, col: usize) -> Self::Elem {
ComplexScalar::conj(&self.inner.eval_elem(col, row))
}
}
impl<E: Expr> ExprHermitian<ExprHermitian<E>>
where
E::Elem: ComplexScalar,
{
pub fn simplify(self) -> E {
self.inner.inner
}
}
impl<'a, T: Scalar + bytemuck::Zeroable + Zero, R: Expr<Elem = T>> core::ops::Add<R>
for ExprLeaf<'a, T>
{
type Output = ExprAdd<Self, R>;
fn add(self, rhs: R) -> Self::Output {
Expr::add(self, rhs)
}
}
impl<'a, T: Scalar + bytemuck::Zeroable + Zero, R: Expr<Elem = T>> core::ops::Sub<R>
for ExprLeaf<'a, T>
{
type Output = ExprSub<Self, R>;
fn sub(self, rhs: R) -> Self::Output {
Expr::sub(self, rhs)
}
}
impl<'a, T: Scalar + bytemuck::Zeroable + Zero> core::ops::Neg for ExprLeaf<'a, T> {
type Output = ExprNeg<Self>;
fn neg(self) -> Self::Output {
Expr::neg(self)
}
}
impl<L1, R1, L2: Expr<Elem = L1::Elem>> core::ops::Add<L2> for ExprAdd<L1, R1>
where
L1: Expr,
R1: Expr<Elem = L1::Elem>,
{
type Output = ExprAdd<Self, L2>;
fn add(self, rhs: L2) -> Self::Output {
Expr::add(self, rhs)
}
}
impl<L1, R1, L2: Expr<Elem = L1::Elem>> core::ops::Sub<L2> for ExprAdd<L1, R1>
where
L1: Expr,
R1: Expr<Elem = L1::Elem>,
{
type Output = ExprSub<Self, L2>;
fn sub(self, rhs: L2) -> Self::Output {
Expr::sub(self, rhs)
}
}
impl<L1, R1> core::ops::Neg for ExprAdd<L1, R1>
where
L1: Expr,
R1: Expr<Elem = L1::Elem>,
{
type Output = ExprNeg<Self>;
fn neg(self) -> Self::Output {
Expr::neg(self)
}
}
impl<E, R: Expr<Elem = E::Elem>> core::ops::Add<R> for ExprScale<E>
where
E: Expr,
{
type Output = ExprAdd<Self, R>;
fn add(self, rhs: R) -> Self::Output {
Expr::add(self, rhs)
}
}
impl<E, R: Expr<Elem = E::Elem>> core::ops::Sub<R> for ExprScale<E>
where
E: Expr,
{
type Output = ExprSub<Self, R>;
fn sub(self, rhs: R) -> Self::Output {
Expr::sub(self, rhs)
}
}
impl<E> core::ops::Neg for ExprScale<E>
where
E: Expr,
{
type Output = ExprNeg<Self>;
fn neg(self) -> Self::Output {
Expr::neg(self)
}
}
impl<E, R: Expr<Elem = E::Elem>> core::ops::Add<R> for ExprTranspose<E>
where
E: Expr,
{
type Output = ExprAdd<Self, R>;
fn add(self, rhs: R) -> Self::Output {
Expr::add(self, rhs)
}
}
impl<E, R: Expr<Elem = E::Elem>> core::ops::Sub<R> for ExprTranspose<E>
where
E: Expr,
{
type Output = ExprSub<Self, R>;
fn sub(self, rhs: R) -> Self::Output {
Expr::sub(self, rhs)
}
}
impl<E> core::ops::Neg for ExprTranspose<E>
where
E: Expr,
{
type Output = ExprNeg<Self>;
fn neg(self) -> Self::Output {
Expr::neg(self)
}
}
pub struct ExprFma<L: Expr, R: Expr<Elem = L::Elem>> {
lhs: L,
rhs: R,
alpha: L::Elem,
beta: L::Elem,
}
impl<L: Expr, R: Expr<Elem = L::Elem>> ExprFma<L, R> {
pub fn new(lhs: L, rhs: R, alpha: L::Elem, beta: L::Elem) -> Self {
assert_eq!(
lhs.shape(),
rhs.shape(),
"Matrix dimensions must match for fused multiply-add"
);
Self {
lhs,
rhs,
alpha,
beta,
}
}
}
impl<L: Expr, R: Expr<Elem = L::Elem>> Expr for ExprFma<L, R> {
type Elem = L::Elem;
fn nrows(&self) -> usize {
self.lhs.nrows()
}
fn ncols(&self) -> usize {
self.lhs.ncols()
}
fn eval_elem(&self, row: usize, col: usize) -> Self::Elem {
self.alpha * self.lhs.eval_elem(row, col) + self.beta * self.rhs.eval_elem(row, col)
}
}
pub struct ExprGemm<A: Expr, B: Expr<Elem = A::Elem>, C: Expr<Elem = A::Elem>> {
a: A,
b: B,
c: C,
alpha: A::Elem,
beta: A::Elem,
_marker: PhantomData<A::Elem>,
}
impl<A: Expr, B: Expr<Elem = A::Elem>, C: Expr<Elem = A::Elem>> ExprGemm<A, B, C> {
pub fn new(a: A, b: B, c: C, alpha: A::Elem, beta: A::Elem) -> Self {
assert_eq!(
a.ncols(),
b.nrows(),
"Matrix dimensions must be compatible for multiplication (A.ncols == B.nrows)"
);
assert_eq!(
a.nrows(),
c.nrows(),
"Matrix dimensions must match for accumulation (A.nrows == C.nrows)"
);
assert_eq!(
b.ncols(),
c.ncols(),
"Matrix dimensions must match for accumulation (B.ncols == C.ncols)"
);
Self {
a,
b,
c,
alpha,
beta,
_marker: PhantomData,
}
}
}
impl<A: Expr, B: Expr<Elem = A::Elem>, C: Expr<Elem = A::Elem>> Expr for ExprGemm<A, B, C> {
type Elem = A::Elem;
fn nrows(&self) -> usize {
self.a.nrows()
}
fn ncols(&self) -> usize {
self.b.ncols()
}
fn eval_elem(&self, row: usize, col: usize) -> Self::Elem {
let k = self.a.ncols();
let mut sum = Self::Elem::zero();
for kk in 0..k {
sum += self.a.eval_elem(row, kk) * self.b.eval_elem(kk, col);
}
self.alpha * sum + self.beta * self.c.eval_elem(row, col)
}
fn eval_into(&self, target: &mut Mat<Self::Elem>) {
assert_eq!(
target.shape(),
self.shape(),
"eval_into: target shape must match expression shape"
);
let a = self.a.eval();
let b = self.b.eval();
let c = self.c.eval();
let k = self.a.ncols();
let (nrows, ncols) = self.shape();
for col in 0..ncols {
for row in 0..nrows {
let mut sum = Self::Elem::zero();
for kk in 0..k {
sum += a[(row, kk)] * b[(kk, col)];
}
target[(row, col)] = self.alpha * sum + self.beta * c[(row, col)];
}
}
}
}
pub fn fma<L: Expr, R: Expr<Elem = L::Elem>>(
alpha: L::Elem,
a: L,
beta: L::Elem,
b: R,
) -> ExprFma<L, R> {
ExprFma::new(a, b, alpha, beta)
}
pub fn gemm<A: Expr, B: Expr<Elem = A::Elem>, C: Expr<Elem = A::Elem>>(
alpha: A::Elem,
a: A,
b: B,
beta: A::Elem,
c: C,
) -> ExprGemm<A, B, C> {
ExprGemm::new(a, b, c, alpha, beta)
}
#[cfg(test)]
mod tests {
use super::*;
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
thread_local! {
static ALLOC_COUNT: Cell<u64> = const { Cell::new(0) };
static ALLOC_ACTIVE: Cell<bool> = const { Cell::new(false) };
}
struct CountingAllocator;
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if ALLOC_ACTIVE.with(Cell::get) {
ALLOC_COUNT.with(|c| c.set(c.get() + 1));
}
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout);
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
if ALLOC_ACTIVE.with(Cell::get) {
ALLOC_COUNT.with(|c| c.set(c.get() + 1));
}
System.alloc_zeroed(layout)
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
if ALLOC_ACTIVE.with(Cell::get) {
ALLOC_COUNT.with(|c| c.set(c.get() + 1));
}
System.realloc(ptr, layout, new_size)
}
}
#[global_allocator]
static COUNTING_GLOBAL: CountingAllocator = CountingAllocator;
fn measure_allocations<R>(body: impl FnOnce() -> R) -> (R, u64) {
ALLOC_COUNT.with(|c| c.set(0));
ALLOC_ACTIVE.with(|a| a.set(true));
let result = body();
ALLOC_ACTIVE.with(|a| a.set(false));
let count = ALLOC_COUNT.with(Cell::get);
(result, count)
}
#[test]
fn test_lazy_leaf() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let expr = a.as_ref().lazy();
let result = expr.eval();
assert_eq!(result[(0, 0)], 1.0);
assert_eq!(result[(1, 1)], 4.0);
}
#[test]
fn test_lazy_add() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]);
let expr = a.as_ref().lazy() + b.as_ref().lazy();
let result = expr.eval();
assert_eq!(result[(0, 0)], 6.0); assert_eq!(result[(0, 1)], 8.0); assert_eq!(result[(1, 0)], 10.0); assert_eq!(result[(1, 1)], 12.0); }
#[test]
fn test_lazy_sub() {
let a: Mat<f64> = Mat::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let expr = a.as_ref().lazy() - b.as_ref().lazy();
let result = expr.eval();
assert_eq!(result[(0, 0)], 4.0); assert_eq!(result[(0, 1)], 4.0); assert_eq!(result[(1, 0)], 4.0); assert_eq!(result[(1, 1)], 4.0); }
#[test]
fn test_lazy_neg() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let expr = -a.as_ref().lazy();
let result = expr.eval();
assert_eq!(result[(0, 0)], -1.0);
assert_eq!(result[(1, 1)], -4.0);
}
#[test]
fn test_lazy_scale() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let expr = a.as_ref().lazy().scale(2.0);
let result = expr.eval();
assert_eq!(result[(0, 0)], 2.0);
assert_eq!(result[(0, 1)], 4.0);
assert_eq!(result[(1, 0)], 6.0);
assert_eq!(result[(1, 1)], 8.0);
}
#[test]
fn test_lazy_transpose() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
let expr = a.as_ref().lazy().t();
let result = expr.eval();
assert_eq!(result.shape(), (3, 2));
assert_eq!(result[(0, 0)], 1.0);
assert_eq!(result[(1, 0)], 2.0);
assert_eq!(result[(2, 0)], 3.0);
assert_eq!(result[(0, 1)], 4.0);
assert_eq!(result[(1, 1)], 5.0);
assert_eq!(result[(2, 1)], 6.0);
}
#[test]
fn test_lazy_matmul() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]);
let expr = a.as_ref().lazy().matmul(b.as_ref().lazy());
let result = expr.eval();
assert_eq!(result[(0, 0)], 19.0);
assert_eq!(result[(0, 1)], 22.0);
assert_eq!(result[(1, 0)], 43.0);
assert_eq!(result[(1, 1)], 50.0);
}
#[test]
fn test_lazy_chained() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]);
let c: Mat<f64> = Mat::from_rows(&[&[1.0, 1.0], &[1.0, 1.0]]);
let expr = (a.as_ref().lazy() + b.as_ref().lazy()) - c.as_ref().lazy();
let result = expr.eval();
assert_eq!(result[(0, 0)], 5.0); assert_eq!(result[(0, 1)], 7.0); assert_eq!(result[(1, 0)], 9.0); assert_eq!(result[(1, 1)], 11.0); }
#[test]
fn test_lazy_complex_conj() {
use num_complex::Complex64;
let a: Mat<Complex64> = Mat::filled(2, 2, Complex64::new(0.0, 0.0));
let mut a = a;
a[(0, 0)] = Complex64::new(1.0, 2.0);
a[(0, 1)] = Complex64::new(3.0, 4.0);
a[(1, 0)] = Complex64::new(5.0, 6.0);
a[(1, 1)] = Complex64::new(7.0, 8.0);
let expr = a.as_ref().lazy().conj();
let result = expr.eval();
assert_eq!(result[(0, 0)], Complex64::new(1.0, -2.0));
assert_eq!(result[(0, 1)], Complex64::new(3.0, -4.0));
assert_eq!(result[(1, 0)], Complex64::new(5.0, -6.0));
assert_eq!(result[(1, 1)], Complex64::new(7.0, -8.0));
}
#[test]
fn test_lazy_hermitian() {
use num_complex::Complex64;
let mut a: Mat<Complex64> = Mat::filled(2, 2, Complex64::new(0.0, 0.0));
a[(0, 0)] = Complex64::new(1.0, 2.0);
a[(0, 1)] = Complex64::new(3.0, 4.0);
a[(1, 0)] = Complex64::new(5.0, 6.0);
a[(1, 1)] = Complex64::new(7.0, 8.0);
let expr = a.as_ref().lazy().h();
let result = expr.eval();
assert_eq!(result.shape(), (2, 2));
assert_eq!(result[(0, 0)], Complex64::new(1.0, -2.0));
assert_eq!(result[(1, 0)], Complex64::new(3.0, -4.0));
assert_eq!(result[(0, 1)], Complex64::new(5.0, -6.0));
assert_eq!(result[(1, 1)], Complex64::new(7.0, -8.0));
}
#[test]
fn test_double_transpose_simplify() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let expr = a.as_ref().lazy().t().t();
let simplified = expr.simplify();
let result = simplified.eval();
assert_eq!(result[(0, 0)], 1.0);
assert_eq!(result[(1, 1)], 4.0);
}
#[test]
fn test_double_scale_simplify() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let expr = a.as_ref().lazy().scale(2.0).scale(3.0);
let simplified = expr.simplify();
let result = simplified.eval();
assert_eq!(result[(0, 0)], 6.0);
assert_eq!(result[(1, 1)], 24.0);
}
#[test]
fn test_fma() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]);
let expr = fma(2.0, a.as_ref().lazy(), 3.0, b.as_ref().lazy());
let result = expr.eval();
assert_eq!(result[(0, 0)], 2.0 * 1.0 + 3.0 * 5.0); assert_eq!(result[(0, 1)], 2.0 * 2.0 + 3.0 * 6.0); assert_eq!(result[(1, 0)], 2.0 * 3.0 + 3.0 * 7.0); assert_eq!(result[(1, 1)], 2.0 * 4.0 + 3.0 * 8.0); }
#[test]
fn test_gemm() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[1.0, 0.0], &[0.0, 1.0]]);
let c: Mat<f64> = Mat::from_rows(&[&[10.0, 10.0], &[10.0, 10.0]]);
let expr = gemm(
2.0,
a.as_ref().lazy(),
b.as_ref().lazy(),
1.0,
c.as_ref().lazy(),
);
let result = expr.eval();
assert_eq!(result[(0, 0)], 2.0 * 1.0 + 10.0); assert_eq!(result[(0, 1)], 2.0 * 2.0 + 10.0); assert_eq!(result[(1, 0)], 2.0 * 3.0 + 10.0); assert_eq!(result[(1, 1)], 2.0 * 4.0 + 10.0); }
#[test]
fn test_eval_into() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]);
let expr = a.as_ref().lazy() + b.as_ref().lazy();
let mut result: Mat<f64> = Mat::zeros(2, 2);
expr.eval_into(&mut result);
assert_eq!(result[(0, 0)], 6.0);
assert_eq!(result[(1, 1)], 12.0);
}
#[test]
#[should_panic(expected = "Matrix dimensions must match for addition")]
fn test_add_shape_mismatch_panics() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]); let b: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0]]); let _ = a.as_ref().lazy().add(b.as_ref().lazy());
}
#[test]
#[should_panic(expected = "Matrix dimensions must match for subtraction")]
fn test_sub_shape_mismatch_panics() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]); let b: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0]]); let _ = a.as_ref().lazy().sub(b.as_ref().lazy());
}
#[test]
#[should_panic(expected = "Matrix dimensions must be compatible for multiplication")]
fn test_matmul_shape_mismatch_panics() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]); let b: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0]]); let _ = a.as_ref().lazy().matmul(b.as_ref().lazy());
}
#[test]
#[should_panic(expected = "Matrix dimensions must match for fused multiply-add")]
fn test_fma_shape_mismatch_panics() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]); let b: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0]]); let _ = fma(2.0, a.as_ref().lazy(), 3.0, b.as_ref().lazy());
}
#[test]
#[should_panic(expected = "Matrix dimensions must be compatible for multiplication")]
fn test_gemm_shape_mismatch_panics() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]); let b: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0]]); let c: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]); let _ = gemm(
1.0,
a.as_ref().lazy(),
b.as_ref().lazy(),
1.0,
c.as_ref().lazy(),
);
}
#[test]
#[should_panic(expected = "eval_into: target shape must match expression shape")]
fn test_eval_into_shape_mismatch_panics() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]); let b: Mat<f64> = Mat::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]); let expr = a.as_ref().lazy() + b.as_ref().lazy();
let mut target: Mat<f64> = Mat::zeros(3, 3);
expr.eval_into(&mut target);
}
#[test]
#[should_panic(expected = "eval_into: target shape must match expression shape")]
fn test_matmul_eval_into_shape_mismatch_panics() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]); let b: Mat<f64> = Mat::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]); let expr = a.as_ref().lazy().matmul(b.as_ref().lazy()); let mut target: Mat<f64> = Mat::zeros(2, 3);
expr.eval_into(&mut target);
}
#[test]
fn test_fma_zero_intermediate_allocation() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]);
let mut target: Mat<f64> = Mat::zeros(2, 2);
let _ = measure_allocations(|| ());
let expr = fma(2.0, a.as_ref().lazy(), 3.0, b.as_ref().lazy());
let ((), allocations) = measure_allocations(|| {
expr.eval_into(&mut target);
});
assert_eq!(
allocations, 0,
"fused FMA eval_into must not perform any intermediate heap allocation"
);
assert_eq!(target[(0, 0)], 2.0 * 1.0 + 3.0 * 5.0);
assert_eq!(target[(0, 1)], 2.0 * 2.0 + 3.0 * 6.0);
assert_eq!(target[(1, 0)], 2.0 * 3.0 + 3.0 * 7.0);
assert_eq!(target[(1, 1)], 2.0 * 4.0 + 3.0 * 8.0);
}
#[test]
fn test_elementwise_chain_zero_intermediate_allocation() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]);
let c: Mat<f64> = Mat::from_rows(&[&[1.0, 1.0], &[1.0, 1.0]]);
let mut target: Mat<f64> = Mat::zeros(2, 2);
let _ = measure_allocations(|| ());
let expr = fma(2.0, a.as_ref().lazy(), 3.0, b.as_ref().lazy())
.sub(c.as_ref().lazy())
.t();
let ((), allocations) = measure_allocations(|| {
expr.eval_into(&mut target);
});
assert_eq!(
allocations, 0,
"fused element-wise chain eval_into must not allocate"
);
assert_eq!(target[(0, 0)], 16.0);
assert_eq!(target[(1, 0)], 21.0);
assert_eq!(target[(0, 1)], 26.0);
assert_eq!(target[(1, 1)], 31.0);
}
#[test]
fn test_counting_allocator_detects_allocation() {
let _ = measure_allocations(|| ());
let (v, allocations) = measure_allocations(|| {
let mut v: Vec<u8> = Vec::with_capacity(4096);
v.push(1);
v
});
assert_eq!(v.len(), 1);
assert!(
allocations >= 1,
"counting allocator must observe a real allocation"
);
}
}