use std::ops::{Index, IndexMut};
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Mat {
nrows: usize,
ncols: usize,
data: Vec<f64>,
}
impl Mat {
pub(crate) fn zeros(nrows: usize, ncols: usize) -> Self {
Self {
nrows,
ncols,
data: vec![0.0; nrows * ncols],
}
}
pub(crate) fn from_fn(
nrows: usize,
ncols: usize,
mut f: impl FnMut(usize, usize) -> f64,
) -> Self {
let mut data = Vec::with_capacity(nrows * ncols);
for j in 0..ncols {
for i in 0..nrows {
data.push(f(i, j));
}
}
Self { nrows, ncols, data }
}
pub(crate) fn nrows(&self) -> usize {
self.nrows
}
pub(crate) fn ncols(&self) -> usize {
self.ncols
}
pub(crate) fn zip_map(&self, other: &Self, f: impl Fn(f64, f64) -> f64) -> Self {
assert_eq!((self.nrows, self.ncols), (other.nrows, other.ncols));
Self {
nrows: self.nrows,
ncols: self.ncols,
data: self
.data
.iter()
.zip(&other.data)
.map(|(&a, &b)| f(a, b))
.collect(),
}
}
pub(crate) fn map(&self, f: impl Fn(f64) -> f64) -> Self {
Self {
nrows: self.nrows,
ncols: self.ncols,
data: self.data.iter().map(|&a| f(a)).collect(),
}
}
}
impl Index<(usize, usize)> for Mat {
type Output = f64;
#[inline]
fn index(&self, (i, j): (usize, usize)) -> &f64 {
debug_assert!(i < self.nrows && j < self.ncols);
&self.data[i + j * self.nrows]
}
}
impl IndexMut<(usize, usize)> for Mat {
#[inline]
fn index_mut(&mut self, (i, j): (usize, usize)) -> &mut f64 {
debug_assert!(i < self.nrows && j < self.ncols);
&mut self.data[i + j * self.nrows]
}
}
pub(crate) fn matmul(dst: &mut Mat, a: &Mat, b: &Mat, k: usize) {
assert!(
k <= a.ncols && k <= b.nrows,
"matmul: k = {k} is out of range"
);
let (m, n) = (a.nrows, b.ncols);
if dst.nrows != m || dst.ncols != n {
*dst = Mat::zeros(m, n);
}
if m == 0 || n == 0 {
return;
}
if k == 0 {
dst.data.fill(0.0);
return;
}
assert!(m + (k - 1) * m <= a.data.len());
assert!(k + (n - 1) * b.nrows <= b.data.len());
assert!(m + (n - 1) * m <= dst.data.len());
let (lda, ldb) = (m as isize, b.nrows as isize);
#[allow(unsafe_code)]
unsafe {
matrixmultiply::dgemm(
m,
k,
n,
1.0,
a.data.as_ptr(),
1,
lda,
b.data.as_ptr(),
1,
ldb,
0.0,
dst.data.as_mut_ptr(),
1,
lda,
);
}
}
pub(crate) fn product(a: &Mat, b: &Mat) -> Mat {
assert_eq!(a.ncols, b.nrows, "product: the shapes do not match");
let mut out = Mat::default();
matmul(&mut out, a, b, a.ncols);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_the_naive_product() {
let mut seed = 1u64;
let mut rand = || {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(seed >> 11) as f64 / (1u64 << 53) as f64 - 0.5
};
let mut c = Mat::default();
for (m, n, k) in [
(1, 1, 1),
(7, 3, 5),
(9, 5, 257),
(130, 13, 600),
(300, 61, 40),
(3, 2100, 15),
(5, 7, 0),
(0, 4, 3),
(4, 0, 3),
] {
let a = Mat::from_fn(m, k + 3, |_, _| rand());
let b = Mat::from_fn(k + 2, n, |_, _| rand());
matmul(&mut c, &a, &b, k);
assert_eq!((c.nrows(), c.ncols()), (m, n));
for i in 0..m {
for j in 0..n {
let want: f64 = (0..k).map(|p| a[(i, p)] * b[(p, j)]).sum();
let size: f64 = (0..k).map(|p| (a[(i, p)] * b[(p, j)]).abs()).sum();
assert!(
(c[(i, j)] - want).abs() <= 1e-14 * size,
"{m}×{n}×{k} [{i}, {j}]: {} vs {want}",
c[(i, j)]
);
}
}
}
}
}