lazymatrix 0.6.0

Lazy normalized design matrices: apply column centering/scaling without materializing (X - 1cᵀ)S⁻¹, preserving sparsity.
Documentation
use std::convert::Infallible;

use lazymatrix::{
    DenseNormalize, EagerMatrix, LazyMatrix, MatVec, MaterializeDense, MatrixErrorType,
    MatrixOwned, MatrixShape, MatrixWrite, NormalizationParams,
};

#[derive(Clone, Debug)]
struct Dense {
    rows: usize,
    columns: usize,
    values: Vec<f64>,
}

impl MatrixShape for Dense {
    fn nrows(&self) -> usize {
        self.rows
    }
    fn ncols(&self) -> usize {
        self.columns
    }
}

impl MatrixErrorType for Dense {
    type Error = Infallible;
}

impl MatrixWrite<f64> for Dense {
    fn set(&mut self, row: usize, column: usize, value: f64) {
        self.values[row * self.columns + column] = value;
    }
}

impl MatrixOwned<f64> for Dense {
    fn zeros(rows: usize, columns: usize) -> Self {
        Self {
            rows,
            columns,
            values: vec![0.0; rows * columns],
        }
    }
}

impl MaterializeDense<f64> for Dense {
    fn materialize_normalized_into<O: MatrixWrite<f64> + ?Sized>(
        &self,
        centers: Option<&[f64]>,
        scales: Option<&[f64]>,
        out: &mut O,
    ) -> Result<(), Infallible> {
        for row in 0..self.rows {
            for col in 0..self.columns {
                let mut value = self.values[row * self.columns + col];
                if let Some(c) = centers {
                    value -= c[col];
                }
                if let Some(s) = scales {
                    value /= s[col];
                }
                out.set(row, col, value);
            }
        }
        Ok(())
    }
}

impl DenseNormalize<f64> for Dense {
    fn normalize_in_place(&mut self, centers: Option<&[f64]>, scales: Option<&[f64]>) {
        for row in self.values.chunks_mut(self.columns) {
            for (col, value) in row.iter_mut().enumerate() {
                if let Some(c) = centers {
                    *value -= c[col];
                }
                if let Some(s) = scales {
                    *value /= s[col];
                }
            }
        }
    }
}

impl MatVec<Vec<f64>> for Dense {
    fn matvec(&self, x: &Vec<f64>) -> Result<Vec<f64>, Infallible> {
        Ok(self
            .values
            .chunks(self.columns)
            .map(|row| row.iter().zip(x).map(|(a, b)| a * b).sum())
            .collect())
    }
}

fn source() -> Dense {
    Dense {
        rows: 2,
        columns: 2,
        values: vec![2.0, 4.0, 6.0, 8.0],
    }
}

#[test]
fn materialization_retains_parameters_without_applying_them_twice() {
    fn forward<M: MatVec<Vec<f64>>, F>(
        matrix: &EagerMatrix<M, F>,
        coefficients: &Vec<f64>,
    ) -> Result<Vec<f64>, M::Error> {
        matrix.matvec(coefficients)
    }

    let source = source();
    let lazy = LazyMatrix::from_parts(&source, Some(vec![2.0, 4.0]), Some(vec![2.0, -2.0]));
    let eager: EagerMatrix<Dense> = lazy.to_eager().unwrap();
    assert_eq!(eager.data().values, [0.0, -0.0, 2.0, -2.0]);
    assert_eq!(eager.centers(), lazy.centers());
    assert_eq!(eager.scales(), lazy.scales());
    assert_eq!(forward(&eager, &vec![1.0, 2.0]).unwrap(), [0.0, -2.0]);
    assert_eq!(source.values, [2.0, 4.0, 6.0, 8.0]);
    let prediction = LazyMatrix::from_normalization(&source, eager.normalization().clone());
    assert_eq!(prediction.matvec(&vec![1.0, 2.0]).unwrap(), [0.0, -2.0]);
}

#[test]
fn consuming_conversion_reuses_dense_storage() {
    let source = source();
    let pointer = source.values.as_ptr();
    let eager = LazyMatrix::with_centers(source, vec![2.0, 4.0]).into_eager();
    assert_eq!(eager.data().values.as_ptr(), pointer);
    assert_eq!(eager.data().values, [0.0, 0.0, 4.0, 4.0]);
}

#[test]
fn reusable_output_provides_a_working_borrowed_operator() {
    let lazy = LazyMatrix::with_scales(source(), vec![2.0, -2.0]);
    let mut destination = Dense::zeros(2, 2);
    let eager = lazy.to_eager_into(&mut destination).unwrap();
    assert_eq!(eager.matvec(&vec![1.0, 2.0]).unwrap(), [-3.0, -5.0]);
    assert_eq!(eager.scales(), Some(&[2.0, -2.0][..]));
}

#[test]
fn output_dimensions_are_checked_before_writing() {
    let lazy = LazyMatrix::<_, f64>::from_parts(source(), None, None);
    let mut destination = Dense::zeros(1, 2);
    destination.values.fill(99.0);
    let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        let _ = lazy.to_eager_into(&mut destination);
    }));
    assert!(panic.is_err());
    assert_eq!(destination.values, [99.0, 99.0]);
}

#[test]
fn fitted_parameters_check_shape_even_when_both_axes_are_inactive() {
    let params = NormalizationParams::<f64>::from_parts(3, None, None);
    assert!(
        std::panic::catch_unwind(|| { LazyMatrix::from_normalization(source(), params) }).is_err()
    );
}

#[test]
fn reusable_output_wrapper_outlives_its_source() {
    let mut destination = Dense::zeros(2, 2);
    let eager = {
        let lazy = LazyMatrix::with_centers(source(), vec![2.0, 4.0]);
        lazy.to_eager_into(&mut destination).unwrap()
    };
    assert_eq!(eager.matvec(&vec![1.0, 1.0]).unwrap(), [0.0, 8.0]);
}

#[test]
#[should_panic(expected = "dense allocation size overflow")]
fn allocating_conversion_checks_size_before_allocating_or_reading() {
    let source = Dense {
        rows: usize::MAX,
        columns: 2,
        values: vec![],
    };
    let _ = LazyMatrix::<_, f64>::from_parts(source, None, None).to_eager::<Dense>();
}