use super::kernels::{self, KernelError, KernelFloat, NormOrd};
use crate::broadcast::broadcast_shapes;
use crate::element::{add, mul};
use crate::layout::advance;
use crate::{Array, Element, Error, Float, Num, Result};
use std::any::{Any, TypeId};
use std::borrow::Cow;
use std::sync::Arc;
pub trait LinalgFloat: Float + KernelFloat {}
impl<T: Float + KernelFloat> LinalgFloat for T {}
const EIGEN_FAILED: &str = "Eigenvalues did not converge";
const SVD_FAILED: &str = "SVD did not converge";
fn shape_error(message: impl Into<String>) -> Error {
Error::Shape(message.into())
}
fn kernel_error(error: KernelError, no_convergence: &str) -> Error {
match error {
KernelError::Singular => Error::Singular,
KernelError::NotPositiveDefinite => Error::NotPositiveDefinite,
KernelError::NoConvergence => Error::NoConvergence(no_convergence.into()),
KernelError::Shape => shape_error("Last 2 dimensions of the array must be square"),
}
}
fn eigen_error(error: KernelError) -> Error {
kernel_error(error, EIGEN_FAILED)
}
fn svd_error(error: KernelError) -> Error {
kernel_error(error, SVD_FAILED)
}
fn or_panic<T>(result: std::result::Result<T, KernelError>, no_convergence: &str) -> T {
result.unwrap_or_else(|error| panic!("{}", kernel_error(error, no_convergence)))
}
fn fail<T>(result: Result<T>) -> T {
result.unwrap_or_else(|error| panic!("{error}"))
}
fn data<T: Element>(a: &Array<T>) -> Cow<'_, [T]> {
match a.as_slice() {
Some(slice) => Cow::Borrowed(slice),
None => Cow::Owned(a.to_vec()),
}
}
fn matrix_shape<T: Element>(a: &Array<T>) -> Result<(usize, usize)> {
if a.ndim() != 2 {
return Err(shape_error(format!(
"{}-dimensional array given. Array must be two-dimensional",
a.ndim()
)));
}
Ok((a.shape()[0], a.shape()[1]))
}
fn square_shape<T: Element>(a: &Array<T>) -> Result<usize> {
let (rows, cols) = matrix_shape(a)?;
if rows != cols {
return Err(shape_error("Last 2 dimensions of the array must be square"));
}
Ok(rows)
}
fn core_strides<T: Element>(a: &Array<T>, left: bool) -> (isize, isize) {
let strides = a.strides();
match (a.ndim(), left) {
(1, true) => (0, strides[0]),
(1, false) => (strides[0], 0),
(n, _) => (strides[n - 2], strides[n - 1]),
}
}
struct Operand<T> {
array: Array<T>,
batch: Vec<isize>,
row: isize,
col: isize,
}
fn operand<T: Num>(
a: &Array<T>,
left: bool,
batch_shape: &[usize],
strided_ok: bool,
) -> Operand<T> {
let array = if strided_ok {
a.clone()
} else {
a.to_contiguous()
};
let (row, col) = core_strides(&array, left);
let core = if array.ndim() == 1 { 1 } else { 2 };
let own_batch = array.ndim() - core;
let mut batch = vec![0; batch_shape.len()];
for axis in 0..own_batch {
if array.shape()[axis] != 1 {
batch[batch_shape.len() - own_batch + axis] = array.strides()[axis];
}
}
Operand {
array,
batch,
row,
col,
}
}
fn for_each_batch<T: Element>(
batch_shape: &[usize],
left: &Operand<T>,
right: &Operand<T>,
mut f: impl FnMut(usize, usize, usize),
) {
let batches: usize = batch_shape.iter().product();
let mut coordinate = vec![0; batch_shape.len()];
let (_, left_offset, _) = left.array.raw_parts();
let (_, right_offset, _) = right.array.raw_parts();
for index in 0..batches {
let mut a_start = left_offset as isize;
let mut b_start = right_offset as isize;
for (axis, &c) in coordinate.iter().enumerate() {
a_start += c as isize * left.batch[axis];
b_start += c as isize * right.batch[axis];
}
f(index, a_start as usize, b_start as usize);
advance(&mut coordinate, batch_shape);
}
}
fn gemm_generic<T: Num>(
(m, k, n): (usize, usize, usize),
a: &[T],
a_strides: (isize, isize),
b: &[T],
b_strides: (isize, isize),
c: &mut [T],
) {
for i in 0..m {
for p in 0..k {
let left = a[(i as isize * a_strides.0 + p as isize * a_strides.1) as usize];
let row = &mut c[i * n..(i + 1) * n];
for (j, slot) in row.iter_mut().enumerate() {
let right = b[(p as isize * b_strides.0 + j as isize * b_strides.1) as usize];
*slot = add(*slot, mul(left, right));
}
}
}
}
fn storage_as<T: Element, U: Element>(a: &Array<T>) -> Option<&[U]> {
(&a.data as &dyn Any)
.downcast_ref::<Arc<Vec<U>>>()
.map(|data| data.as_slice())
}
impl<T: Num> Array<T> {
pub fn matmul(&self, other: &Array<T>) -> Array<T> {
let mut out = Array::default();
self.matmul_write(other, &mut out, true, T::ONE, None);
out
}
pub fn matmul_into(&self, other: &Array<T>, out: &mut Array<T>) {
self.matmul_write(other, out, false, T::ONE, None);
}
fn matmul_write(
&self,
other: &Array<T>,
output: &mut Array<T>,
allocate: bool,
alpha: T,
map: Option<fn(T) -> T>,
) {
for (index, array) in [self, other].into_iter().enumerate() {
assert!(
array.ndim() > 0,
"matmul: Input operand {index} does not have enough dimensions (has 0, gufunc core with signature (n?,k),(k,m?)->(n?,m?) requires 1)"
);
}
let (left_vector, right_vector) = (self.ndim() == 1, other.ndim() == 1);
let (m, k) = if left_vector {
(1, self.shape()[0])
} else {
(self.shape()[self.ndim() - 2], self.shape()[self.ndim() - 1])
};
let (other_k, n) = if right_vector {
(other.shape()[0], 1)
} else {
(
other.shape()[other.ndim() - 2],
other.shape()[other.ndim() - 1],
)
};
assert!(
k == other_k,
"matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size {other_k} is different from {k})"
);
let left_batch = &self.shape()[..self.ndim() - if left_vector { 1 } else { 2 }];
let right_batch = &other.shape()[..other.ndim() - if right_vector { 1 } else { 2 }];
let batch_shape = broadcast_shapes(left_batch, right_batch).unwrap_or_else(|| {
panic!(
"operands could not be broadcast together with remapped shapes [original->remapped]: {:?} {:?}",
self.shape(),
other.shape()
)
});
let mut output_shape = batch_shape.clone();
if !left_vector {
output_shape.push(m);
}
if !right_vector {
output_shape.push(n);
}
if allocate {
*output = Array::from_vec(vec![T::ZERO; output_shape.iter().product()], &output_shape);
} else {
assert_eq!(
output.shape(),
output_shape,
"matmul: output operand has a mismatch in its core dimension"
);
assert!(
output.is_contiguous(),
"matmul: output array is not C-contiguous"
);
}
let native =
TypeId::of::<T>() == TypeId::of::<f64>() || TypeId::of::<T>() == TypeId::of::<f32>();
let left = operand(self, true, &batch_shape, native);
let right = operand(other, false, &batch_shape, native);
let block = m * n;
let total = batch_shape.iter().product::<usize>() * block;
if total == 0 {
return;
}
let (a_strides, b_strides) = ((left.row, left.col), (right.row, right.col));
let offset = output.offset;
let out = Arc::make_mut(&mut output.data);
macro_rules! native_product {
($float:ty) => {
if let (Some(a), Some(b)) = (
storage_as::<T, $float>(&left.array),
storage_as::<T, $float>(&right.array),
) {
let out = (out as &mut dyn Any)
.downcast_mut::<Vec<$float>>()
.expect("same type");
let alpha = *(&alpha as &dyn Any)
.downcast_ref::<$float>()
.expect("same type");
let map = *(&map as &dyn Any)
.downcast_ref::<Option<fn($float) -> $float>>()
.expect("same type");
for_each_batch(&batch_shape, &left, &right, |index, a_start, b_start| {
super::packed_gemm::run(
(m, k, n),
alpha,
super::packed_gemm::Matrix {
data: a,
offset: a_start,
row: a_strides.0,
col: a_strides.1,
},
super::packed_gemm::Matrix {
data: b,
offset: b_start,
row: b_strides.0,
col: b_strides.1,
},
0.0,
&mut out[offset + index * block..offset + (index + 1) * block],
0,
(n as isize, 1),
map,
);
});
return;
}
};
}
native_product!(f64);
native_product!(f32);
let out = &mut out[offset..offset + total];
out.fill(T::ZERO);
if k != 0 {
let (a, _, _) = left.array.raw_parts();
let (b, _, _) = right.array.raw_parts();
for_each_batch(&batch_shape, &left, &right, |index, a_start, b_start| {
gemm_generic(
(m, k, n),
&a[a_start..],
a_strides,
&b[b_start..],
b_strides,
&mut out[index * block..(index + 1) * block],
);
});
}
}
pub fn dot(&self, other: &Array<T>) -> Array<T> {
assert!(
self.ndim() <= 2 && other.ndim() <= 2,
"dot only supports arrays with at most 2 dimensions"
);
if self.ndim() == 0 || other.ndim() == 0 {
return self.zip_map(other.clone(), mul);
}
self.matmul(other)
}
pub fn trace(&self) -> T {
assert!(
self.ndim() == 2,
"diag requires an array of at least two dimensions"
);
let (storage, offset, strides) = self.raw_parts();
let diagonal = strides[0] + strides[1];
(0..self.shape()[0].min(self.shape()[1])).fold(T::ZERO, |sum, i| {
add(
sum,
storage[(offset as isize + i as isize * diagonal) as usize],
)
})
}
}
pub fn gemm<F: LinalgFloat>(
alpha: F,
a: &Array<F>,
b: &Array<F>,
beta: F,
c: &Array<F>,
) -> Array<F> {
let (m, k) = fail(matrix_shape(a));
let (bk, n) = fail(matrix_shape(b));
assert_eq!(
k, bk,
"matmul: Input operand 1 has a mismatch in its core dimension 0"
);
assert_eq!(
c.shape(),
[m, n],
"gemm: output operand has a mismatch in its core dimension"
);
let mut out = if beta == <F as Num>::ZERO {
vec![<F as Num>::ZERO; m * n]
} else {
c.to_vec()
};
macro_rules! typed {
($float:ty) => {
if let (Some(ad), Some(bd)) = (storage_as::<F, $float>(a), storage_as::<F, $float>(b)) {
let scalar = |x: &F| *(x as &dyn Any).downcast_ref::<$float>().expect("same type");
super::packed_gemm::run(
(m, k, n),
scalar(&alpha),
super::packed_gemm::Matrix {
data: ad,
offset: a.offset,
row: a.strides()[0],
col: a.strides()[1],
},
super::packed_gemm::Matrix {
data: bd,
offset: b.offset,
row: b.strides()[0],
col: b.strides()[1],
},
scalar(&beta),
(&mut out as &mut dyn Any)
.downcast_mut::<Vec<$float>>()
.expect("same type"),
0,
(n as isize, 1),
None,
);
return Array::from_vec(out, &[m, n]);
}
};
}
typed!(f64);
typed!(f32);
unreachable!("LinalgFloat is sealed to f32 and f64")
}
pub fn matmul_map<F: LinalgFloat>(a: &Array<F>, b: &Array<F>, alpha: F, f: fn(F) -> F) -> Array<F> {
let mut out = Array::default();
a.matmul_write(b, &mut out, true, alpha, Some(f));
out
}
pub fn outer<T: Num>(a: &Array<T>, b: &Array<T>) -> Array<T> {
let (left, right) = (data(a), data(b));
let output = left
.iter()
.flat_map(|&x| right.iter().map(move |&y| mul(x, y)))
.collect();
Array::from_vec(output, &[left.len(), right.len()])
}
pub fn inner<T: Num>(a: &Array<T>, b: &Array<T>) -> T {
assert!(
a.ndim() == 1 && b.ndim() == 1,
"inner: expected two 1-dimensional arrays"
);
assert!(
a.size() == b.size(),
"shapes {:?} and {:?} not aligned: {} (dim 0) != {} (dim 0)",
a.shape(),
b.shape(),
a.size(),
b.size()
);
data(a)
.iter()
.zip(data(b).iter())
.fold(T::ZERO, |sum, (&x, &y)| add(sum, mul(x, y)))
}
pub fn det<F: LinalgFloat>(a: &Array<F>) -> F {
let n = fail(square_shape(a));
match kernels::Lu::factor(&data(a), n) {
Ok(factor) => factor.det(),
Err(_) => <F as Num>::ZERO,
}
}
pub fn inv<F: LinalgFloat>(a: &Array<F>) -> Result<Array<F>> {
let n = square_shape(a)?;
kernels::inv(&data(a), n)
.map(|inverse| Array::from_vec(inverse, &[n, n]))
.map_err(eigen_error)
}
pub fn solve<F: LinalgFloat>(a: &Array<F>, b: &Array<F>) -> Result<Array<F>> {
let n = square_shape(a)?;
if b.ndim() != 1 && b.ndim() != 2 {
return Err(shape_error(
"solve: Input operand 1 must have one or two dimensions",
));
}
if b.shape()[0] != n {
return Err(shape_error(
"solve: Input operand 1 has a mismatch in its core dimension 0",
));
}
let rhs_cols = if b.ndim() == 1 { 1 } else { b.shape()[1] };
let solution = kernels::Lu::factor(&data(a), n)
.and_then(|factor| factor.solve(&data(b), rhs_cols))
.map_err(eigen_error)?;
let shape: &[usize] = if b.ndim() == 1 { &[n] } else { &[n, rhs_cols] };
Ok(Array::from_vec(solution, shape))
}
pub fn lstsq<F: LinalgFloat>(a: &Array<F>, b: &Array<F>) -> Result<Array<F>> {
let (rows, cols) = matrix_shape(a)?;
if b.ndim() != 1 && b.ndim() != 2 {
return Err(shape_error("expected a vector or matrix right-hand side"));
}
if b.shape()[0] != rows {
return Err(shape_error("Incompatible dimensions"));
}
let rhs_cols = if b.ndim() == 1 { 1 } else { b.shape()[1] };
let solution = kernels::lstsq(&data(a), rows, cols, &data(b), rhs_cols).map_err(svd_error)?;
let shape: &[usize] = if b.ndim() == 1 {
&[cols]
} else {
&[cols, rhs_cols]
};
Ok(Array::from_vec(solution, shape))
}
pub fn pinv<F: LinalgFloat>(a: &Array<F>) -> Array<F> {
let (rows, cols) = fail(matrix_shape(a));
Array::from_vec(
or_panic(kernels::pinv(&data(a), rows, cols), SVD_FAILED),
&[cols, rows],
)
}
pub fn norm<F: LinalgFloat>(a: &Array<F>) -> F {
kernels::norm(&data(a))
}
pub fn norm_axis<F: LinalgFloat>(a: &Array<F>, ord: NormOrd, axis: isize) -> Array<F> {
let ndim = a.ndim() as isize;
assert!(
(-ndim..ndim).contains(&axis),
"axis {axis} is out of bounds for array of dimension {ndim}"
);
let axis = if axis < 0 { axis + ndim } else { axis } as usize;
let len = a.shape()[axis];
let mut shape = a.shape().to_vec();
shape.remove(axis);
let count: usize = shape.iter().product();
let moved = a.moveaxis(axis as isize, -1);
let norms =
kernels::norm_axis(&data(&moved), count, len, 1, ord).unwrap_or_else(|error| match error {
KernelError::NoConvergence => panic!("{SVD_FAILED}"),
_ => panic!("Invalid norm order for vectors"),
});
Array::from_vec(norms, &shape)
}
pub fn cholesky<F: LinalgFloat>(a: &Array<F>) -> Result<Array<F>> {
let n = square_shape(a)?;
kernels::cholesky(&data(a), n)
.map(|lower| Array::from_vec(lower, &[n, n]))
.map_err(eigen_error)
}
pub fn qr<F: LinalgFloat>(a: &Array<F>) -> (Array<F>, Array<F>) {
let (rows, cols) = fail(matrix_shape(a));
let (q, r, reduced) = or_panic(kernels::qr(&data(a), rows, cols), EIGEN_FAILED);
(
Array::from_vec(q, &[rows, reduced]),
Array::from_vec(r, &[reduced, cols]),
)
}
pub fn eigh<F: LinalgFloat>(a: &Array<F>) -> (Array<F>, Array<F>) {
let n = fail(square_shape(a));
let (values, vectors) = or_panic(kernels::eigh(&data(a), n), EIGEN_FAILED);
(
Array::from_vec(values, &[n]),
Array::from_vec(vectors, &[n, n]),
)
}
pub fn svd<F: LinalgFloat>(a: &Array<F>) -> (Array<F>, Array<F>, Array<F>) {
let (rows, cols) = fail(matrix_shape(a));
let (u, values, vt, reduced) = or_panic(kernels::svd(&data(a), rows, cols), SVD_FAILED);
(
Array::from_vec(u, &[rows, reduced]),
Array::from_vec(values, &[reduced]),
Array::from_vec(vt, &[reduced, cols]),
)
}
pub fn matrix_rank<F: LinalgFloat>(a: &Array<F>) -> usize {
let (rows, cols) = fail(matrix_shape(a));
or_panic(kernels::matrix_rank(&data(a), rows, cols), SVD_FAILED)
}
pub fn matrix_power<F: LinalgFloat>(a: &Array<F>, n: i32) -> Result<Array<F>> {
let size = square_shape(a)?;
kernels::matrix_power(&data(a), size, i64::from(n))
.map(|power| Array::from_vec(power, &[size, size]))
.map_err(eigen_error)
}
#[allow(clippy::type_complexity)]
pub fn eig<F: LinalgFloat>(a: &Array<F>) -> Result<(Array<F>, Array<F>, Array<F>, Array<F>)> {
let n = square_shape(a)?;
kernels::eig(&data(a), n)
.map(|(w_re, w_im, v_re, v_im)| {
(
Array::from_vec(w_re, &[n]),
Array::from_vec(w_im, &[n]),
Array::from_vec(v_re, &[n, n]),
Array::from_vec(v_im, &[n, n]),
)
})
.map_err(eigen_error)
}
pub fn eigvals<F: LinalgFloat>(a: &Array<F>) -> Result<(Array<F>, Array<F>)> {
let n = square_shape(a)?;
kernels::eigvals(&data(a), n)
.map(|(re, im)| (Array::from_vec(re, &[n]), Array::from_vec(im, &[n])))
.map_err(eigen_error)
}
pub fn eigvalsh<F: LinalgFloat>(a: &Array<F>) -> Array<F> {
let n = fail(square_shape(a));
Array::from_vec(or_panic(kernels::eigvalsh(&data(a), n), EIGEN_FAILED), &[n])
}
pub fn slogdet<F: LinalgFloat>(a: &Array<F>) -> (F, F) {
let n = fail(square_shape(a));
or_panic(kernels::slogdet(&data(a), n), EIGEN_FAILED)
}
pub fn cond<F: LinalgFloat>(a: &Array<F>) -> F {
let (rows, cols) = fail(matrix_shape(a));
or_panic(kernels::cond(&data(a), rows, cols), SVD_FAILED)
}
pub fn cond_ord<F: LinalgFloat>(a: &Array<F>, ord: NormOrd) -> F {
let (rows, cols) = fail(matrix_shape(a));
kernels::cond_ord(&data(a), rows, cols, ord).unwrap_or_else(|error| match error {
KernelError::NoConvergence => panic!("{SVD_FAILED}"),
_ => panic!("Invalid norm order for matrices"),
})
}
pub fn vector_norm<F: LinalgFloat>(a: &Array<F>, ord: NormOrd) -> F {
kernels::vector_norm(&data(a), ord).unwrap_or_else(|_| panic!("Invalid norm order for vectors"))
}
pub fn matrix_norm<F: LinalgFloat>(a: &Array<F>, ord: NormOrd) -> F {
let (rows, cols) = fail(matrix_shape(a));
kernels::matrix_norm(&data(a), rows, cols, ord).unwrap_or_else(|error| match error {
KernelError::NoConvergence => panic!("{SVD_FAILED}"),
_ => panic!("Invalid norm order for matrices"),
})
}
pub fn multi_dot<T: Num>(arrays: &[&Array<T>]) -> Array<T> {
assert!(arrays.len() >= 2, "Expecting at least two arrays.");
let last = arrays.len() - 1;
let buffers: Vec<Cow<'_, [T]>> = arrays.iter().map(|array| data(array)).collect();
let mut dims = Vec::with_capacity(arrays.len());
for (index, array) in arrays.iter().enumerate() {
dims.push(match array.ndim() {
1 if index == 0 => (1, array.shape()[0]),
1 if index == last => (array.shape()[0], 1),
2 => (array.shape()[0], array.shape()[1]),
_ => {
panic!("multi_dot: the first and last arguments may be 1-D, all others must be 2-D")
}
});
}
let chain: Vec<(&[T], usize, usize)> = buffers
.iter()
.zip(&dims)
.map(|(buffer, &(rows, cols))| (&buffer[..], rows, cols))
.collect();
let (product, rows, cols) =
kernels::multi_dot(&chain).unwrap_or_else(|_| panic!("multi_dot: shapes are not aligned"));
let mut shape = Vec::new();
if arrays[0].ndim() == 2 {
shape.push(rows);
}
if arrays[last].ndim() == 2 {
shape.push(cols);
}
Array::from_vec(product, &shape)
}
fn normalize_axes(axes: &[isize], ndim: usize) -> Vec<usize> {
axes.iter()
.map(|&axis| {
let size = ndim as isize;
assert!(
(-size..size).contains(&axis),
"tensordot: axis {axis} is out of bounds for array of dimension {ndim}"
);
(if axis < 0 { axis + size } else { axis }) as usize
})
.collect()
}
pub fn tensordot<T: Num>(
a: &Array<T>,
b: &Array<T>,
axes_a: &[isize],
axes_b: &[isize],
) -> Array<T> {
let (product, shape) = kernels::tensordot(
&data(a),
a.shape(),
&data(b),
b.shape(),
&normalize_axes(axes_a, a.ndim()),
&normalize_axes(axes_b, b.ndim()),
)
.unwrap_or_else(|_| panic!("shape-mismatch for sum"));
Array::from_vec(product, &shape)
}
pub fn tensordot_n<T: Num>(a: &Array<T>, b: &Array<T>, n: usize) -> Array<T> {
assert!(
n <= a.ndim() && n <= b.ndim(),
"tensordot: cannot contract {n} axes of arrays with {} and {} dimensions",
a.ndim(),
b.ndim()
);
let axes_a: Vec<isize> = (a.ndim() - n..a.ndim()).map(|axis| axis as isize).collect();
let axes_b: Vec<isize> = (0..n).map(|axis| axis as isize).collect();
tensordot(a, b, &axes_a, &axes_b)
}
pub fn vdot<T: Num>(a: &Array<T>, b: &Array<T>) -> T {
kernels::vdot(&data(a), &data(b)).unwrap_or_else(|_| panic!("vectors have different lengths"))
}