use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use scirs2_core::numeric::{Float, NumAssign};
use crate::error::{LinalgError, LinalgResult};
#[allow(dead_code)]
pub fn dot<F>(x: &ArrayView1<F>, y: &ArrayView1<F>) -> LinalgResult<F>
where
F: Float + NumAssign + 'static,
{
if x.len() != y.len() {
return Err(LinalgError::ShapeError(format!(
"Vectors must have the same length for dot product, got {} and {}",
x.len(),
y.len()
)));
}
Ok(x.dot(y))
}
#[allow(dead_code)]
pub fn norm<F>(x: &ArrayView1<F>) -> LinalgResult<F>
where
F: Float + NumAssign + 'static,
{
if x.is_empty() {
return Err(LinalgError::InvalidInputError(
"Cannot compute norm of an empty vector".to_string(),
));
}
let mut sum = F::zero();
for &val in x.iter() {
sum += val * val;
}
Ok(Float::sqrt(sum))
}
#[allow(dead_code)]
pub fn gemv<F>(
alpha: F,
a: &ArrayView2<F>,
x: &ArrayView1<F>,
beta: F,
y: &ArrayView1<F>,
) -> LinalgResult<Array1<F>>
where
F: Float + NumAssign + 'static,
{
if a.ncols() != x.len() {
return Err(LinalgError::ShapeError(format!(
"Matrix columns ({}) must match vector length ({}) for gemv",
a.ncols(),
x.len()
)));
}
if a.nrows() != y.len() {
return Err(LinalgError::ShapeError(format!(
"Matrix rows ({}) must match result vector length ({}) for gemv",
a.nrows(),
y.len()
)));
}
let mut result = y.to_owned();
if beta != F::one() {
result.map_inplace(|v| *v *= beta);
}
let ax = a.dot(x);
result.zip_mut_with(&ax, |y_i, &ax_i| *y_i += alpha * ax_i);
Ok(result)
}
#[allow(dead_code)]
pub fn gemm<F>(
alpha: F,
a: &ArrayView2<F>,
b: &ArrayView2<F>,
beta: F,
c: &ArrayView2<F>,
) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + 'static,
{
if a.ncols() != b.nrows() {
return Err(LinalgError::ShapeError(format!(
"Matrix dimensions not compatible for multiplication: a.ncols ({}) != b.nrows ({})",
a.ncols(),
b.nrows()
)));
}
if a.nrows() != c.nrows() || b.ncols() != c.ncols() {
return Err(LinalgError::ShapeError(format!(
"Output matrix dimensions ({},{}) don't match expected ({},{})",
c.nrows(),
c.ncols(),
a.nrows(),
b.ncols()
)));
}
let mut result = c.to_owned();
if beta != F::one() {
result.map_inplace(|v| *v *= beta);
}
let ab = a.dot(b);
result.zip_mut_with(&ab, |c_ij, &ab_ij| *c_ij += alpha * ab_ij);
Ok(result)
}
#[cfg(feature = "cuda")]
const CUDA_MATMUL_MIN_FLOPS: usize = 1 << 21;
#[allow(dead_code)]
pub fn matmul<F>(a: &ArrayView2<F>, b: &ArrayView2<F>) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + 'static,
{
if a.ncols() != b.nrows() {
return Err(LinalgError::ShapeError(format!(
"Matrix dimensions not compatible for multiplication: a.ncols ({}) != b.nrows ({})",
a.ncols(),
b.nrows()
)));
}
#[cfg(feature = "cuda")]
{
use std::any::TypeId;
if TypeId::of::<F>() == TypeId::of::<f64>()
&& crate::gpu_cuda::cuda_is_available()
&& a.nrows()
.saturating_mul(a.ncols())
.saturating_mul(b.ncols())
>= CUDA_MATMUL_MIN_FLOPS
{
let a_f64: &ArrayView2<f64> = unsafe { std::mem::transmute(a) };
let b_f64: &ArrayView2<f64> = unsafe { std::mem::transmute(b) };
if let Ok(c_f64) = crate::gpu_cuda::cuda_gemm(a_f64, b_f64) {
let c_view = c_f64.view();
let c_view_f: &ArrayView2<F> = unsafe { std::mem::transmute(&c_view) };
return Ok(c_view_f.to_owned());
}
}
}
Ok(a.dot(b))
}
#[allow(dead_code)]
pub fn solve<F>(a: &ArrayView2<F>, b: &ArrayView1<F>) -> LinalgResult<Array1<F>>
where
F: Float + NumAssign + 'static,
{
if a.nrows() != a.ncols() {
return Err(LinalgError::ShapeError(format!(
"Matrix must be square for solve, got shape {:?}",
a.shape()
)));
}
if a.nrows() != b.len() {
return Err(LinalgError::ShapeError(format!(
"Matrix rows ({}) must match vector length ({}) for solve",
a.nrows(),
b.len()
)));
}
let n = a.nrows();
let mut aug = Array2::<F>::zeros((n, n + 1));
for i in 0..n {
for j in 0..n {
aug[[i, j]] = a[[i, j]];
}
aug[[i, n]] = b[i];
}
for i in 0..n {
let mut max_row = i;
let mut max_val = Float::abs(aug[[i, i]]);
for j in (i + 1)..n {
let val = Float::abs(aug[[j, i]]);
if val > max_val {
max_row = j;
max_val = val;
}
}
if max_val < F::epsilon() {
return Err(LinalgError::SingularMatrixError(
"Matrix is singular or nearly singular".to_string(),
));
}
if max_row != i {
for j in 0..(n + 1) {
let temp = aug[[i, j]];
aug[[i, j]] = aug[[max_row, j]];
aug[[max_row, j]] = temp;
}
}
for j in (i + 1)..n {
let factor = aug[[j, i]] / aug[[i, i]];
aug[[j, i]] = F::zero();
for k in (i + 1)..(n + 1) {
aug[[j, k]] = aug[[j, k]] - factor * aug[[i, k]];
}
}
}
let mut x = Array1::<F>::zeros(n);
for i in (0..n).rev() {
let mut sum = aug[[i, n]];
for j in (i + 1)..n {
sum -= aug[[i, j]] * x[j];
}
x[i] = sum / aug[[i, i]];
}
Ok(x)
}
#[allow(dead_code)]
pub fn inv<F>(a: &ArrayView2<F>) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + 'static,
{
if a.nrows() != a.ncols() {
return Err(LinalgError::ShapeError(format!(
"Matrix must be square for inverse, got shape {:?}",
a.shape()
)));
}
let n = a.nrows();
let mut aug = Array2::<F>::zeros((n, 2 * n));
for i in 0..n {
for j in 0..n {
aug[[i, j]] = a[[i, j]];
}
aug[[i, i + n]] = F::one(); }
for i in 0..n {
let mut max_row = i;
let mut max_val = Float::abs(aug[[i, i]]);
for j in (i + 1)..n {
let val = Float::abs(aug[[j, i]]);
if val > max_val {
max_row = j;
max_val = val;
}
}
if max_val < F::epsilon() {
return Err(LinalgError::SingularMatrixError(
"Matrix is singular or nearly singular".to_string(),
));
}
if max_row != i {
for j in 0..(2 * n) {
let temp = aug[[i, j]];
aug[[i, j]] = aug[[max_row, j]];
aug[[max_row, j]] = temp;
}
}
let pivot = aug[[i, i]];
for j in 0..(2 * n) {
aug[[i, j]] /= pivot;
}
for j in 0..n {
if j != i {
let factor = aug[[j, i]];
for k in 0..(2 * n) {
aug[[j, k]] = aug[[j, k]] - factor * aug[[i, k]];
}
}
}
}
let mut a_inv = Array2::<F>::zeros((n, n));
for i in 0..n {
for j in 0..n {
a_inv[[i, j]] = aug[[i, j + n]];
}
}
Ok(a_inv)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use scirs2_core::ndarray::{array, Array1, Array2};
#[test]
fn test_dot() {
let x = array![1.0, 2.0, 3.0];
let y = array![4.0, 5.0, 6.0];
let result = dot(&x.view(), &y.view()).expect("Operation failed");
assert_relative_eq!(result, 32.0, epsilon = 1e-10); }
#[test]
fn test_norm() {
let x = array![3.0, 4.0];
let result = norm(&x.view()).expect("Operation failed");
assert_relative_eq!(result, 5.0, epsilon = 1e-10); }
#[test]
fn test_gemv() {
let a = array![[1.0, 2.0], [3.0, 4.0]];
let x = array![2.0, 3.0];
let y = Array1::<f64>::zeros(2);
let result = gemv(1.0, &a.view(), &x.view(), 0.0, &y.view()).expect("Operation failed");
assert_relative_eq!(result[0], 8.0, epsilon = 1e-10); assert_relative_eq!(result[1], 18.0, epsilon = 1e-10); }
#[test]
fn test_gemm() {
let a = array![[1.0, 2.0], [3.0, 4.0]];
let b = array![[5.0, 6.0], [7.0, 8.0]];
let c = Array2::<f64>::zeros((2, 2));
let result = gemm(1.0, &a.view(), &b.view(), 0.0, &c.view()).expect("Operation failed");
assert_relative_eq!(result[[0, 0]], 19.0, epsilon = 1e-10); assert_relative_eq!(result[[0, 1]], 22.0, epsilon = 1e-10); assert_relative_eq!(result[[1, 0]], 43.0, epsilon = 1e-10); assert_relative_eq!(result[[1, 1]], 50.0, epsilon = 1e-10); }
#[test]
fn test_matmul() {
let a = array![[1.0, 2.0], [3.0, 4.0]];
let b = array![[5.0, 6.0], [7.0, 8.0]];
let result = matmul(&a.view(), &b.view()).expect("Operation failed");
assert_relative_eq!(result[[0, 0]], 19.0, epsilon = 1e-10); assert_relative_eq!(result[[0, 1]], 22.0, epsilon = 1e-10); assert_relative_eq!(result[[1, 0]], 43.0, epsilon = 1e-10); assert_relative_eq!(result[[1, 1]], 50.0, epsilon = 1e-10); }
#[test]
fn test_solve() {
let a = array![[3.0, 1.0], [1.0, 2.0]];
let b = array![9.0, 8.0];
let x = solve(&a.view(), &b.view()).expect("Operation failed");
assert_relative_eq!(x[0], 2.0, epsilon = 1e-10);
assert_relative_eq!(x[1], 3.0, epsilon = 1e-10);
let b_check = a.dot(&x);
assert_relative_eq!(b_check[0], b[0], epsilon = 1e-10);
assert_relative_eq!(b_check[1], b[1], epsilon = 1e-10);
}
#[cfg(feature = "cuda")]
#[test]
fn matmul_cuda_dispatch_f64_or_skip() {
if !crate::gpu_cuda::cuda_is_available() {
eprintln!("skipping: no NVIDIA CUDA device — GPU branch not engaged");
return;
}
let m = 130usize;
let k = 130usize;
let n = 130usize;
let a_data: Vec<f64> = (0..m * k)
.map(|idx| (idx as f64 + 1.0) / (m * k) as f64)
.collect();
let b_data: Vec<f64> = (0..k * n)
.map(|idx| (idx as f64 * 1.3 + 0.7) / (k * n) as f64)
.collect();
let a = Array2::from_shape_vec((m, k), a_data).expect("a shape ok");
let b = Array2::from_shape_vec((k, n), b_data).expect("b shape ok");
let c_matmul = matmul(&a.view(), &b.view()).expect("matmul f64 130x130 failed");
let c_cpu = a.dot(&b);
assert_eq!(c_matmul.shape(), &[m, n]);
let max_rel = c_matmul
.iter()
.zip(c_cpu.iter())
.map(|(g, e)| (g - e).abs() / e.abs().max(1e-30))
.fold(0.0f64, f64::max);
assert!(
max_rel < 1e-6,
"130x130 f64 GPU dispatch: max relative diff {max_rel} exceeds 1e-6"
);
}
#[cfg(feature = "cuda")]
#[test]
fn matmul_f32_stays_cpu() {
let m = 130usize;
let k = 130usize;
let n = 130usize;
let a_data: Vec<f32> = (0..m * k)
.map(|idx| (idx as f32 + 1.0) / (m * k) as f32)
.collect();
let b_data: Vec<f32> = (0..k * n)
.map(|idx| (idx as f32 * 1.3 + 0.7) / (k * n) as f32)
.collect();
let a = Array2::<f32>::from_shape_vec((m, k), a_data).expect("a shape ok");
let b = Array2::<f32>::from_shape_vec((k, n), b_data).expect("b shape ok");
let c_matmul = matmul(&a.view(), &b.view()).expect("matmul f32 130x130 failed");
let c_cpu = a.dot(&b);
assert_eq!(c_matmul.shape(), &[m, n]);
let max_diff = c_matmul
.iter()
.zip(c_cpu.iter())
.map(|(g, e)| (g - e).abs())
.fold(0.0f32, f32::max);
assert!(
max_diff < 1e-5,
"f32 130x130 (CPU path): max abs diff {max_diff} exceeds 1e-5"
);
}
#[cfg(feature = "cuda")]
#[test]
fn matmul_subthreshold_f64_stays_cpu() {
let a = array![
[1.0_f64, 2.0, 3.0, 4.0],
[5.0, 6.0, 7.0, 8.0],
[9.0, 10.0, 11.0, 12.0],
[13.0, 14.0, 15.0, 16.0]
];
let b = array![
[0.1_f64, 0.2, 0.3, 0.4],
[0.5, 0.6, 0.7, 0.8],
[0.9, 1.0, 1.1, 1.2],
[1.3, 1.4, 1.5, 1.6]
];
let c_matmul = matmul(&a.view(), &b.view()).expect("sub-threshold matmul failed");
let c_dot = a.dot(&b);
c_matmul
.iter()
.zip(c_dot.iter())
.enumerate()
.for_each(|(i, (m_val, d_val))| {
assert_eq!(
*m_val, *d_val,
"sub-threshold matmul element {i} not bit-identical to a.dot(&b)"
);
});
}
#[test]
fn matmul_fallback_safety() {
let a_bad = Array2::<f64>::zeros((3, 4));
let b_bad = Array2::<f64>::zeros((5, 2)); assert!(
matmul(&a_bad.view(), &b_bad.view()).is_err(),
"shape mismatch must return Err, not panic"
);
let a = array![[1.0_f64, 0.0], [0.0, 1.0]];
let b = array![[3.0_f64, 1.0], [2.0, 4.0]];
let c = matmul(&a.view(), &b.view()).expect("identity x B failed");
assert_relative_eq!(c[[0, 0]], 3.0, epsilon = 1e-13);
assert_relative_eq!(c[[0, 1]], 1.0, epsilon = 1e-13);
assert_relative_eq!(c[[1, 0]], 2.0, epsilon = 1e-13);
assert_relative_eq!(c[[1, 1]], 4.0, epsilon = 1e-13);
}
#[test]
fn test_inv() {
let a = array![[4.0, 7.0], [2.0, 6.0]];
let a_inv = inv(&a.view()).expect("Operation failed");
assert_relative_eq!(a_inv[[0, 0]], 0.6, epsilon = 1e-10);
assert_relative_eq!(a_inv[[0, 1]], -0.7, epsilon = 1e-10);
assert_relative_eq!(a_inv[[1, 0]], -0.2, epsilon = 1e-10);
assert_relative_eq!(a_inv[[1, 1]], 0.4, epsilon = 1e-10);
let identity = a.dot(&a_inv);
assert_relative_eq!(identity[[0, 0]], 1.0, epsilon = 1e-10);
assert_relative_eq!(identity[[0, 1]], 0.0, epsilon = 1e-10);
assert_relative_eq!(identity[[1, 0]], 0.0, epsilon = 1e-10);
assert_relative_eq!(identity[[1, 1]], 1.0, epsilon = 1e-10);
}
}