use scirs2_autograd as ag;
pub use ag::tensor_ops;
pub use ag::{Context, Float, Tensor};
pub mod matrix_calculus;
pub mod einsum;
pub use einsum::{einsum, einsum_grad, EinsumError};
pub mod helpers {
use super::*;
pub fn trace_workaround<'g, F: ag::Float>(
matrix: &ag::Tensor<'g, F>,
n: usize,
ctx: &'g ag::Context<'g, F>,
) -> ag::Tensor<'g, F> {
let mut eye_data = vec![F::zero(); n * n];
for i in 0..n {
eye_data[i * n + i] = F::one();
}
let eye = ag::tensor_ops::convert_to_tensor(
ag::ndarray::Array2::from_shape_vec((n, n), eye_data).expect("Operation failed"),
ctx,
);
let diag_elements = matrix * eye;
ag::tensor_ops::sum_all(diag_elements)
}
pub fn eye_workaround<'g, F: ag::Float>(
n: usize,
ctx: &'g ag::Context<'g, F>,
) -> ag::Tensor<'g, F> {
let mut eye_data = vec![F::zero(); n * n];
for i in 0..n {
eye_data[i * n + i] = F::one();
}
ag::tensor_ops::convert_to_tensor(
ag::ndarray::Array2::from_shape_vec((n, n), eye_data).expect("Operation failed"),
ctx,
)
}
pub fn diag_workaround<'g, F: ag::Float>(
diagonal: &ag::Tensor<'g, F>,
ctx: &'g ag::Context<'g, F>,
) -> ag::Tensor<'g, F> {
let diagarray = diagonal
.eval(ctx)
.expect("diag_workaround: failed to evaluate diagonal tensor");
let n = diagarray.len();
let mut matrix_data = vec![F::zero(); n * n];
for i in 0..n {
matrix_data[i * n + i] = diagarray[i];
}
ag::tensor_ops::convert_to_tensor(
ag::ndarray::Array2::from_shape_vec((n, n), matrix_data).expect("Operation failed"),
ctx,
)
}
pub fn frobenius_norm<'g, F: ag::Float>(matrix: &ag::Tensor<'g, F>) -> ag::Tensor<'g, F> {
let squared = matrix * matrix;
let sum_squared = ag::tensor_ops::sum_all(squared);
ag::tensor_ops::sqrt(sum_squared)
}
pub fn det_approximation<'g, F: ag::Float>(
matrix: &ag::Tensor<'g, F>,
n: usize,
ctx: &'g ag::Context<'g, F>,
) -> ag::Tensor<'g, F> {
let mat = matrix
.eval(ctx)
.expect("det_approximation: failed to evaluate matrix tensor");
let mut lu = vec![F::zero(); n * n];
for i in 0..n {
for j in 0..n {
lu[i * n + j] = mat[[i, j]];
}
}
let mut det = F::one();
for k in 0..n {
let mut pivot_row = k;
let mut pivot_mag = lu[k * n + k].abs();
for i in (k + 1)..n {
let candidate = lu[i * n + k].abs();
if candidate > pivot_mag {
pivot_mag = candidate;
pivot_row = i;
}
}
if pivot_mag == F::zero() {
det = F::zero();
break;
}
if pivot_row != k {
for j in 0..n {
lu.swap(pivot_row * n + j, k * n + j);
}
det = -det;
}
let pivot = lu[k * n + k];
for i in (k + 1)..n {
let factor = lu[i * n + k] / pivot;
for j in k..n {
let updated = lu[i * n + j] - factor * lu[k * n + j];
lu[i * n + j] = updated;
}
}
det *= pivot;
}
ag::tensor_ops::convert_to_tensor(ag::ndarray::Array2::from_elem((1, 1), det), ctx)
}
pub fn solve_iterative<'g, F: ag::Float>(
a: &ag::Tensor<'g, F>,
b: &ag::Tensor<'g, F>,
iterations: usize,
learning_rate: F,
ctx: &'g ag::Context<'g, F>,
) -> ag::Tensor<'g, F> {
let barray = ag::integration::tensor_conversion::to_ndarray_with_context(b, ctx)
.expect("solve_iterative: failed to evaluate b tensor");
let n = barray.len();
let mut x = ag::tensor_ops::convert_to_tensor(ag::ndarray::Array2::zeros((n, 1)), ctx);
let lr_tensor = ag::tensor_ops::convert_to_tensor(
ag::ndarray::Array2::from_elem((1, 1), learning_rate),
ctx,
);
for _iter in 0..iterations {
let ax = ag::tensor_ops::matmul(a, x);
let residual = ax - b;
let at = ag::tensor_ops::transpose(a, &[1, 0]);
let gradient = ag::tensor_ops::matmul(at, residual);
let update = gradient * lr_tensor;
x = x - update;
}
x
}
pub fn dominant_eigenvalue<'g, F: ag::Float>(
matrix: &ag::Tensor<'g, F>,
iterations: usize,
n: usize,
ctx: &'g ag::Context<'g, F>,
) -> ag::Tensor<'g, F> {
let mut v_data = vec![F::one(); n];
v_data[0] = F::one();
for (i, item) in v_data.iter_mut().enumerate().take(n).skip(1) {
*item = F::from(0.1).expect("Operation failed")
* F::from(i as f64).expect("Operation failed");
}
let mut v = ag::tensor_ops::convert_to_tensor(
ag::ndarray::Array2::from_shape_vec((n, 1), v_data).expect("Operation failed"),
ctx,
);
for _iter in 0..iterations {
let av = ag::tensor_ops::matmul(matrix, v);
let norm = frobenius_norm(&av);
v = av / norm;
}
let vt = ag::tensor_ops::transpose(v, &[1, 0]);
let av = ag::tensor_ops::matmul(matrix, v);
let numerator = ag::tensor_ops::matmul(vt, av);
let denominator = ag::tensor_ops::matmul(vt, v);
numerator / denominator
}
pub fn condition_number_approx<'g, F: ag::Float>(
matrix: &ag::Tensor<'g, F>,
iterations: usize,
n: usize,
ctx: &'g ag::Context<'g, F>,
) -> ag::Tensor<'g, F> {
let lambda_max = dominant_eigenvalue(matrix, iterations, n, ctx);
lambda_max
}
pub fn rank_approximation<'g, F: ag::Float>(
matrix: &ag::Tensor<'g, F>,
tolerance: F,
ctx: &'g ag::Context<'g, F>,
) -> ag::Tensor<'g, F> {
let mat = matrix
.eval(ctx)
.expect("rank_approximation: failed to evaluate matrix tensor");
let shape = mat.shape();
assert!(
shape.len() == 2,
"rank_approximation: expected a 2D matrix, got shape {shape:?}"
);
let m = shape[0];
let n = shape[1];
if m == 0 || n == 0 {
return ag::tensor_ops::convert_to_tensor(
ag::ndarray::Array2::from_elem((1, 1), F::zero()),
ctx,
);
}
let p = m.min(n);
let mut gram = vec![F::zero(); p * p];
if m >= n {
for i in 0..n {
for j in i..n {
let mut acc = F::zero();
for k in 0..m {
acc += mat[[k, i]] * mat[[k, j]];
}
gram[i * p + j] = acc;
gram[j * p + i] = acc;
}
}
} else {
for i in 0..m {
for j in i..m {
let mut acc = F::zero();
for k in 0..n {
acc += mat[[i, k]] * mat[[j, k]];
}
gram[i * p + j] = acc;
gram[j * p + i] = acc;
}
}
}
let eigenvalues = jacobi_eigenvalues(&mut gram, p);
let mut singular_values: Vec<F> = eigenvalues
.into_iter()
.map(|lambda| {
if lambda > F::zero() {
lambda.sqrt()
} else {
F::zero()
}
})
.collect();
singular_values.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let sigma_max = singular_values.first().copied().unwrap_or(F::zero());
let dim = F::from(m.max(n) as f64).unwrap_or(F::one());
let relative_floor = dim * F::epsilon().sqrt() * sigma_max;
let threshold = if tolerance > relative_floor {
tolerance
} else {
relative_floor
};
let rank = singular_values
.iter()
.filter(|&&sigma| sigma > threshold)
.count();
ag::tensor_ops::convert_to_tensor(
ag::ndarray::Array2::from_elem((1, 1), F::from(rank as f64).unwrap_or_else(F::zero)),
ctx,
)
}
fn jacobi_eigenvalues<F: ag::Float>(a: &mut [F], n: usize) -> Vec<F> {
if n == 0 {
return Vec::new();
}
if n == 1 {
return vec![a[0]];
}
let max_sweeps = 100usize;
for _sweep in 0..max_sweeps {
let mut off = F::zero();
for p in 0..n {
for q in (p + 1)..n {
off += a[p * n + q] * a[p * n + q];
}
}
if off <= F::zero() {
break;
}
let mut diag_scale = F::zero();
for i in 0..n {
diag_scale += a[i * n + i] * a[i * n + i];
}
let eps = F::epsilon();
if off.sqrt() <= eps * (diag_scale.sqrt() + F::epsilon()) {
break;
}
for p in 0..n {
for q in (p + 1)..n {
let apq = a[p * n + q];
if apq == F::zero() {
continue;
}
let app = a[p * n + p];
let aqq = a[q * n + q];
let two = F::from(2.0).unwrap_or_else(F::one);
let tau = (aqq - app) / (two * apq);
let t = if tau >= F::zero() {
F::one() / (tau + (F::one() + tau * tau).sqrt())
} else {
-F::one() / (-tau + (F::one() + tau * tau).sqrt())
};
let c = F::one() / (F::one() + t * t).sqrt();
let s = t * c;
for k in 0..n {
let akp = a[k * n + p];
let akq = a[k * n + q];
a[k * n + p] = c * akp - s * akq;
a[k * n + q] = s * akp + c * akq;
}
for k in 0..n {
let apk = a[p * n + k];
let aqk = a[q * n + k];
a[p * n + k] = c * apk - s * aqk;
a[q * n + k] = s * apk + c * aqk;
}
}
}
}
(0..n).map(|i| a[i * n + i]).collect()
}
}
#[cfg(test)]
mod determinant_tests {
use super::helpers;
use scirs2_autograd as ag;
#[test]
fn test_det_approximation_3x3_is_real_not_placeholder() {
ag::run(|ctx: &mut ag::Context<f64>| {
let m = ag::tensor_ops::convert_to_tensor(
ag::ndarray::arr2(&[[2.0_f64, 0.0, 1.0], [3.0, 1.0, 2.0], [1.0, 0.0, 3.0]]),
ctx,
);
let det = helpers::det_approximation(&m, 3, ctx);
let det_arr = det.eval(ctx).expect("Test: failed to evaluate determinant");
assert!(
(det_arr[[0, 0]] - 5.0).abs() < 1e-10,
"expected determinant 5.0, got {}",
det_arr[[0, 0]]
);
});
}
#[test]
fn test_det_approximation_singular_is_zero() {
ag::run(|ctx: &mut ag::Context<f64>| {
let m = ag::tensor_ops::convert_to_tensor(
ag::ndarray::arr2(&[[1.0_f64, 2.0, 3.0], [0.0, 1.0, 4.0], [2.0, 4.0, 6.0]]),
ctx,
);
let det = helpers::det_approximation(&m, 3, ctx);
let det_arr = det.eval(ctx).expect("Test: failed to evaluate determinant");
assert!(
det_arr[[0, 0]].abs() < 1e-10,
"expected singular determinant ~0, got {}",
det_arr[[0, 0]]
);
});
}
#[test]
fn test_rank_full_rank_identity() {
ag::run(|ctx: &mut ag::Context<f64>| {
let m = ag::tensor_ops::convert_to_tensor(
ag::ndarray::arr2(&[[1.0_f64, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]),
ctx,
);
let rank = helpers::rank_approximation(&m, 1e-9_f64, ctx);
let rank_arr = rank.eval(ctx).expect("Test: failed to evaluate rank");
assert!(
(rank_arr[[0, 0]] - 3.0).abs() < 1e-12,
"expected rank 3, got {}",
rank_arr[[0, 0]]
);
});
}
#[test]
fn test_rank_rank_deficient_is_not_constant_three() {
ag::run(|ctx: &mut ag::Context<f64>| {
let m = ag::tensor_ops::convert_to_tensor(
ag::ndarray::arr2(&[[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0], [2.0, 4.0, 6.0]]),
ctx,
);
let rank = helpers::rank_approximation(&m, 1e-9_f64, ctx);
let rank_arr = rank.eval(ctx).expect("Test: failed to evaluate rank");
assert!(
(rank_arr[[0, 0]] - 2.0).abs() < 1e-12,
"expected rank 2 (rank-deficient), got {}",
rank_arr[[0, 0]]
);
});
}
#[test]
fn test_rank_rank_one_outer_product() {
ag::run(|ctx: &mut ag::Context<f64>| {
let m = ag::tensor_ops::convert_to_tensor(
ag::ndarray::arr2(&[[2.0_f64, 4.0, 6.0], [3.0, 6.0, 9.0], [1.0, 2.0, 3.0]]),
ctx,
);
let rank = helpers::rank_approximation(&m, 1e-9_f64, ctx);
let rank_arr = rank.eval(ctx).expect("Test: failed to evaluate rank");
assert!(
(rank_arr[[0, 0]] - 1.0).abs() < 1e-12,
"expected rank 1 (outer product), got {}",
rank_arr[[0, 0]]
);
});
}
#[test]
fn test_rank_non_square_default_tolerance() {
ag::run(|ctx: &mut ag::Context<f64>| {
let m = ag::tensor_ops::convert_to_tensor(
ag::ndarray::arr2(&[[1.0_f64, 0.0, 2.0], [0.0, 1.0, 3.0]]),
ctx,
);
let rank = helpers::rank_approximation(&m, 0.0_f64, ctx);
let rank_arr = rank.eval(ctx).expect("Test: failed to evaluate rank");
assert!(
(rank_arr[[0, 0]] - 2.0).abs() < 1e-12,
"expected rank 2 for 2x3 matrix, got {}",
rank_arr[[0, 0]]
);
});
}
}