pub mod common;
pub use common::Transpose;
mod faer;
use faer::{random_distance_preserving_matrix_impl, sgemm_impl, svd_into_impl};
use rand::Rng;
#[cfg(test)]
mod reference;
#[allow(clippy::too_many_arguments)]
pub fn sgemm(
atranspose: Transpose,
btranspose: Transpose,
m: usize,
n: usize,
k: usize,
alpha: f32,
a: &[f32],
b: &[f32],
beta: Option<f32>,
c: &mut [f32],
) {
assert_eq!(
a.len(),
m * k,
"expected {}x{} matrix `a` to have length {}, instead got {}",
m,
k,
m * k,
a.len()
);
assert_eq!(
b.len(),
k * n,
"expected {}x{} matrix `b` to have length {}, instead got {}",
k,
n,
k * n,
b.len()
);
assert_eq!(
c.len(),
m * n,
"expected {}x{} matrix `c` to have length {}, instead got {}",
m,
n,
m * n,
c.len()
);
sgemm_impl(atranspose, btranspose, m, n, k, alpha, a, b, beta, c)
}
pub fn svd_into(
m: usize,
n: usize,
a: &mut [f32],
singular_values: &mut [f32],
u: &mut [f32],
vt: &mut [f32],
) -> Result<(), impl std::error::Error + 'static> {
assert_eq!(a.len(), m * n);
assert_eq!(singular_values.len(), m.min(n));
assert_eq!(u.len(), m * m);
assert_eq!(vt.len(), n * n);
svd_into_impl(m, n, a, singular_values, u, vt)
}
pub fn random_distance_preserving_matrix<T: Rng + ?Sized>(dim: usize, rng: &mut T) -> Vec<f32> {
random_distance_preserving_matrix_impl(dim, rng)
}
#[cfg(test)]
mod tests {
use approx::{assert_abs_diff_eq, assert_relative_eq};
use rand::{distr::Distribution, rngs::StdRng, SeedableRng};
use rand_distr::StandardNormal;
use serde::Deserialize;
use super::*;
use crate::reference;
#[test]
fn test_reference_implementation() {
let problems = reference::test_sgemm_problems();
for (i, problem) in problems.iter().enumerate() {
let result = problem.check(sgemm);
if let Err(err) = result {
panic!("{} on iteration {}. Problem: {:?}", err, i, problem);
}
}
}
fn test_file_path(name: &str) -> String {
format!("{}/test_data/{}", env!("CARGO_MANIFEST_DIR"), name)
}
const SVD_INPUT_FILE: &str = "reference_svd_inputs.json";
#[derive(Deserialize, Debug)]
struct SVDTestCase {
m: usize,
n: usize,
matrix: Vec<f32>,
singular_values: Vec<f32>,
}
impl SVDTestCase {
fn summary(&self) -> String {
format!("svd test case with dimension {}x{}", self.m, self.n)
}
}
struct SVDTolerance {
absolute: f32,
relative: f32,
}
impl SVDTolerance {
fn check(&self, absolute: f32, relative: f32) -> bool {
absolute <= self.absolute || relative <= self.relative
}
}
fn materialize_singular_values(singular_values: &[f32], m: usize, n: usize) -> Vec<f32> {
assert_eq!(singular_values.len(), m.min(n));
let mut output = vec![0.0; m * n];
for (i, &s) in singular_values.iter().enumerate() {
output[n * i + i] = s;
}
output
}
fn test_svd(
case: &SVDTestCase,
singular_value_tolerance: &SVDTolerance,
reconstructed_tolerance: &SVDTolerance,
context: &dyn std::fmt::Display,
) {
let mut singular_values = vec![0.0; case.m.min(case.n)];
let mut u = vec![0.0; case.m * case.m];
let mut vt = vec![0.0; case.n * case.n];
svd_into(
case.m,
case.n,
&mut case.matrix.clone(),
&mut singular_values,
&mut u,
&mut vt,
)
.unwrap();
for (i, (&got, &expected)) in
std::iter::zip(singular_values.iter(), case.singular_values.iter()).enumerate()
{
let diff = (got - expected).abs();
let relative = diff / expected;
assert!(
singular_value_tolerance.check(diff, relative),
"got {} but expected {} (diff: {}, relative: {}) at position {}: {}",
got,
expected,
diff,
relative,
i,
context
);
}
let full_singular_values = materialize_singular_values(&singular_values, case.m, case.n);
let mut temp = vec![0.0; case.m * case.n];
sgemm(
Transpose::None,
Transpose::None,
case.m,
case.n,
case.m,
1.0,
&u,
&full_singular_values,
None,
&mut temp,
);
let mut output = vec![0.0; case.m * case.n];
sgemm(
Transpose::None,
Transpose::None,
case.m,
case.n,
case.n,
1.0,
&temp,
&vt,
None,
&mut output,
);
for row in 0..case.m {
for col in 0..case.n {
let got = output[case.n * row + col];
let expected = case.matrix[case.n * row + col];
let diff = (got - expected).abs();
let relative = diff / expected;
assert!(
reconstructed_tolerance.check(diff, relative),
"mismatch in reconstructed matrix at (row, col) = ({}, {}). \
Got {}, expected {} (diff: {}, relative: {}). {}",
row,
col,
got,
expected,
diff,
relative,
context
);
}
}
}
#[test]
fn test_svd_implementation() {
let path = test_file_path(SVD_INPUT_FILE);
let file = std::fs::File::open(path.clone())
.unwrap_or_else(|_| panic!("failed to open file {path}"));
let reader = std::io::BufReader::new(file);
let cases: Vec<SVDTestCase> = serde_json::from_reader(reader).unwrap();
let singular_values_tolerance = SVDTolerance {
absolute: 2.0e-6,
relative: 3.0e-6,
};
let reconstructed_tolerance = SVDTolerance {
absolute: 5.0e-5,
relative: 0.0,
};
for (i, case) in cases.iter().enumerate() {
let context = format!(
"while processing case {} of {}: {}",
i + 1,
cases.len(),
case.summary()
);
test_svd(
case,
&singular_values_tolerance,
&reconstructed_tolerance,
&context,
);
}
}
const EPSILON: f32 = 1e-5;
fn test_distance_preserving_matrix_impl(dim: usize, rng: &mut StdRng) {
let q = random_distance_preserving_matrix(dim, rng);
let qm = ::faer::mat::MatRef::from_row_major_slice(&q, dim, dim);
let m = qm * qm.transpose();
for j in 0..dim {
for i in 0..dim {
if i == j {
assert_abs_diff_eq!(m[(i, j)], 1.0, epsilon = EPSILON);
} else {
assert_abs_diff_eq!(m[(i, j)], 0.0, epsilon = EPSILON);
}
}
}
const RANDOM_TRIALS: usize = 100;
let mut v = vec![0.0f32; dim];
for _ in 0..RANDOM_TRIALS {
v.iter_mut()
.for_each(|i| *i = StandardNormal {}.sample(rng));
let vm = ::faer::mat::MatRef::from_row_major_slice(&v, dim, 1);
let v_norm = vm.squared_norm_l2();
let t = qm * vm;
let t_norm = t.squared_norm_l2();
assert_relative_eq!(v_norm, t_norm, epsilon = EPSILON, max_relative = EPSILON);
assert_ne!(vm, t);
}
}
#[test]
fn test_rotation_matrix() {
let mut rng = StdRng::seed_from_u64(0xc0ff33);
let num_trials = 5;
for dim in [2, 100, 256] {
for _ in 0..num_trials {
test_distance_preserving_matrix_impl(dim, &mut rng);
}
}
}
}