use oxiblas_core::scalar::{Field, Real, Scalar};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CooError {
LengthMismatch {
rows_len: usize,
cols_len: usize,
values_len: usize,
},
InvalidRowIndex {
index: usize,
nrows: usize,
},
InvalidColumnIndex {
index: usize,
ncols: usize,
},
}
impl core::fmt::Display for CooError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::LengthMismatch {
rows_len,
cols_len,
values_len,
} => {
write!(
f,
"Length mismatch: rows={rows_len}, cols={cols_len}, values={values_len}"
)
}
Self::InvalidRowIndex { index, nrows } => {
write!(f, "Row index {index} out of bounds for {nrows} rows")
}
Self::InvalidColumnIndex { index, ncols } => {
write!(f, "Column index {index} out of bounds for {ncols} columns")
}
}
}
}
impl std::error::Error for CooError {}
#[derive(Debug, Clone)]
pub struct CooMatrix<T: Scalar> {
nrows: usize,
ncols: usize,
row_indices: Vec<usize>,
col_indices: Vec<usize>,
values: Vec<T>,
}
impl<T: Scalar + Clone> CooMatrix<T> {
pub fn new(
nrows: usize,
ncols: usize,
row_indices: Vec<usize>,
col_indices: Vec<usize>,
values: Vec<T>,
) -> Result<Self, CooError> {
let len = values.len();
if row_indices.len() != len || col_indices.len() != len {
return Err(CooError::LengthMismatch {
rows_len: row_indices.len(),
cols_len: col_indices.len(),
values_len: len,
});
}
for &row in &row_indices {
if row >= nrows {
return Err(CooError::InvalidRowIndex { index: row, nrows });
}
}
for &col in &col_indices {
if col >= ncols {
return Err(CooError::InvalidColumnIndex { index: col, ncols });
}
}
Ok(Self {
nrows,
ncols,
row_indices,
col_indices,
values,
})
}
#[inline]
pub unsafe fn new_unchecked(
nrows: usize,
ncols: usize,
row_indices: Vec<usize>,
col_indices: Vec<usize>,
values: Vec<T>,
) -> Self {
Self {
nrows,
ncols,
row_indices,
col_indices,
values,
}
}
pub fn new_empty(nrows: usize, ncols: usize) -> Self {
Self {
nrows,
ncols,
row_indices: Vec::new(),
col_indices: Vec::new(),
values: Vec::new(),
}
}
pub fn with_capacity(nrows: usize, ncols: usize, capacity: usize) -> Self {
Self {
nrows,
ncols,
row_indices: Vec::with_capacity(capacity),
col_indices: Vec::with_capacity(capacity),
values: Vec::with_capacity(capacity),
}
}
#[inline]
pub fn nrows(&self) -> usize {
self.nrows
}
#[inline]
pub fn ncols(&self) -> usize {
self.ncols
}
#[inline]
pub fn shape(&self) -> (usize, usize) {
(self.nrows, self.ncols)
}
#[inline]
pub fn len(&self) -> usize {
self.values.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
#[inline]
pub fn row_indices(&self) -> &[usize] {
&self.row_indices
}
#[inline]
pub fn col_indices(&self) -> &[usize] {
&self.col_indices
}
#[inline]
pub fn values(&self) -> &[T] {
&self.values
}
#[inline]
pub fn values_mut(&mut self) -> &mut [T] {
&mut self.values
}
pub fn push(&mut self, row: usize, col: usize, value: T) {
assert!(row < self.nrows, "Row index {row} out of bounds");
assert!(col < self.ncols, "Column index {col} out of bounds");
self.row_indices.push(row);
self.col_indices.push(col);
self.values.push(value);
}
#[inline]
pub unsafe fn push_unchecked(&mut self, row: usize, col: usize, value: T) {
self.row_indices.push(row);
self.col_indices.push(col);
self.values.push(value);
}
pub fn iter(&self) -> impl Iterator<Item = (usize, usize, &T)> {
self.row_indices
.iter()
.zip(self.col_indices.iter())
.zip(self.values.iter())
.map(|((&row, &col), val)| (row, col, val))
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = (usize, usize, &mut T)> {
self.row_indices
.iter()
.zip(self.col_indices.iter())
.zip(self.values.iter_mut())
.map(|((&row, &col), val)| (row, col, val))
}
pub fn to_csr(&self) -> crate::csr::CsrMatrix<T>
where
T: Scalar<Real = T> + Field + Real,
{
crate::convert::coo_to_csr(self)
}
pub fn to_csc(&self) -> crate::csc::CscMatrix<T>
where
T: Scalar<Real = T> + Field + Real,
{
crate::convert::coo_to_csc(self)
}
pub fn to_dense(&self) -> oxiblas_matrix::Mat<T>
where
T: Field + bytemuck::Zeroable,
{
let mut dense: oxiblas_matrix::Mat<T> = oxiblas_matrix::Mat::zeros(self.nrows, self.ncols);
for (row, col, val) in self.iter() {
let current = dense[(row, col)].clone();
dense[(row, col)] = current + val.clone();
}
dense
}
pub fn from_dense(dense: &oxiblas_matrix::MatRef<'_, T>) -> Self
where
T: Field,
{
let (nrows, ncols) = dense.shape();
let mut row_indices = Vec::new();
let mut col_indices = Vec::new();
let mut values = Vec::new();
let eps = <T as Scalar>::epsilon();
for i in 0..nrows {
for j in 0..ncols {
let val = dense[(i, j)].clone();
if Scalar::abs(val.clone()) > eps {
row_indices.push(i);
col_indices.push(j);
values.push(val);
}
}
}
Self {
nrows,
ncols,
row_indices,
col_indices,
values,
}
}
pub fn sum_duplicates(&mut self)
where
T: Field,
{
if self.is_empty() {
return;
}
let mut indices: Vec<usize> = (0..self.len()).collect();
indices.sort_by_key(|&i| (self.row_indices[i], self.col_indices[i]));
let mut new_rows = Vec::with_capacity(self.len());
let mut new_cols = Vec::with_capacity(self.len());
let mut new_vals = Vec::with_capacity(self.len());
let mut prev_row = self.row_indices[indices[0]];
let mut prev_col = self.col_indices[indices[0]];
let mut acc = self.values[indices[0]].clone();
for &idx in &indices[1..] {
let row = self.row_indices[idx];
let col = self.col_indices[idx];
if row == prev_row && col == prev_col {
acc = acc + self.values[idx].clone();
} else {
if Scalar::abs(acc.clone()) > T::epsilon() {
new_rows.push(prev_row);
new_cols.push(prev_col);
new_vals.push(acc);
}
prev_row = row;
prev_col = col;
acc = self.values[idx].clone();
}
}
if Scalar::abs(acc.clone()) > T::epsilon() {
new_rows.push(prev_row);
new_cols.push(prev_col);
new_vals.push(acc);
}
self.row_indices = new_rows;
self.col_indices = new_cols;
self.values = new_vals;
}
pub fn scale(&mut self, alpha: T) {
for val in &mut self.values {
*val = val.clone() * alpha.clone();
}
}
pub fn scaled(&self, alpha: T) -> Self {
let mut result = self.clone();
result.scale(alpha);
result
}
pub fn reserve(&mut self, additional: usize) {
self.row_indices.reserve(additional);
self.col_indices.reserve(additional);
self.values.reserve(additional);
}
pub fn clear(&mut self) {
self.row_indices.clear();
self.col_indices.clear();
self.values.clear();
}
}
#[derive(Debug, Clone)]
pub struct CooMatrixBuilder<T: Scalar> {
matrix: CooMatrix<T>,
}
impl<T: Scalar + Clone> CooMatrixBuilder<T> {
pub fn new(nrows: usize, ncols: usize) -> Self {
Self {
matrix: CooMatrix::new_empty(nrows, ncols),
}
}
pub fn with_capacity(nrows: usize, ncols: usize, capacity: usize) -> Self {
Self {
matrix: CooMatrix::with_capacity(nrows, ncols, capacity),
}
}
pub fn add(&mut self, row: usize, col: usize, value: T) -> &mut Self {
self.matrix.push(row, col, value);
self
}
pub fn add_diagonal(&mut self, index: usize, value: T) -> &mut Self {
self.matrix.push(index, index, value);
self
}
pub fn add_block(
&mut self,
start_row: usize,
start_col: usize,
block: &oxiblas_matrix::MatRef<'_, T>,
) -> &mut Self
where
T: Field,
{
let (nrows, ncols) = block.shape();
let eps = <T as Scalar>::epsilon();
for i in 0..nrows {
for j in 0..ncols {
let val = block[(i, j)].clone();
if Scalar::abs(val.clone()) > eps {
self.matrix.push(start_row + i, start_col + j, val);
}
}
}
self
}
pub fn build(self) -> CooMatrix<T> {
self.matrix
}
pub fn build_csr(self) -> crate::csr::CsrMatrix<T>
where
T: Scalar<Real = T> + Field + Real,
{
self.matrix.to_csr()
}
pub fn build_csc(self) -> crate::csc::CscMatrix<T>
where
T: Scalar<Real = T> + Field + Real,
{
self.matrix.to_csc()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_coo_new() {
let row_indices = vec![0, 1, 2];
let col_indices = vec![0, 1, 2];
let values = vec![1.0f64, 2.0, 3.0];
let coo = CooMatrix::new(3, 3, row_indices, col_indices, values).unwrap();
assert_eq!(coo.nrows(), 3);
assert_eq!(coo.ncols(), 3);
assert_eq!(coo.len(), 3);
}
#[test]
fn test_coo_push() {
let mut coo = CooMatrix::<f64>::new_empty(3, 3);
coo.push(0, 0, 1.0);
coo.push(1, 1, 2.0);
coo.push(2, 2, 3.0);
assert_eq!(coo.len(), 3);
assert_eq!(coo.values(), &[1.0, 2.0, 3.0]);
}
#[test]
fn test_coo_with_duplicates() {
let row_indices = vec![0, 0, 1];
let col_indices = vec![0, 0, 1];
let values = vec![1.0f64, 2.0, 3.0];
let mut coo = CooMatrix::new(2, 2, row_indices, col_indices, values).unwrap();
coo.sum_duplicates();
assert_eq!(coo.len(), 2);
let entries: Vec<_> = coo.iter().collect();
assert_eq!(entries[0], (0, 0, &3.0));
assert_eq!(entries[1], (1, 1, &3.0));
}
#[test]
fn test_coo_iter() {
let row_indices = vec![0, 1, 2];
let col_indices = vec![0, 1, 2];
let values = vec![1.0f64, 2.0, 3.0];
let coo = CooMatrix::new(3, 3, row_indices, col_indices, values).unwrap();
let entries: Vec<_> = coo.iter().collect();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0], (0, 0, &1.0));
assert_eq!(entries[1], (1, 1, &2.0));
assert_eq!(entries[2], (2, 2, &3.0));
}
#[test]
fn test_coo_scale() {
let row_indices = vec![0, 1, 2];
let col_indices = vec![0, 1, 2];
let values = vec![1.0f64, 2.0, 3.0];
let mut coo = CooMatrix::new(3, 3, row_indices, col_indices, values).unwrap();
coo.scale(2.0);
assert_eq!(coo.values(), &[2.0, 4.0, 6.0]);
}
#[test]
fn test_coo_builder() {
let mut builder = CooMatrixBuilder::<f64>::with_capacity(3, 3, 5);
builder.add(0, 0, 1.0);
builder.add(1, 1, 2.0);
builder.add(2, 2, 3.0);
builder.add_diagonal(0, 0.5); let csr = builder.build_csr();
assert_eq!(csr.nnz(), 3);
assert_eq!(csr.get(0, 0), Some(&1.5)); }
#[test]
fn test_coo_empty() {
let coo: CooMatrix<f64> = CooMatrix::new_empty(5, 5);
assert!(coo.is_empty());
assert_eq!(coo.nrows(), 5);
assert_eq!(coo.ncols(), 5);
}
#[test]
fn test_coo_clear() {
let mut coo = CooMatrix::<f64>::new_empty(3, 3);
coo.push(0, 0, 1.0);
coo.push(1, 1, 2.0);
assert_eq!(coo.len(), 2);
coo.clear();
assert!(coo.is_empty());
assert_eq!(coo.nrows(), 3); }
#[test]
fn test_coo_invalid_row() {
let row_indices = vec![5];
let col_indices = vec![0];
let values = vec![1.0f64];
let result = CooMatrix::new(3, 3, row_indices, col_indices, values);
assert!(matches!(result, Err(CooError::InvalidRowIndex { .. })));
}
#[test]
fn test_coo_invalid_col() {
let row_indices = vec![0];
let col_indices = vec![5];
let values = vec![1.0f64];
let result = CooMatrix::new(3, 3, row_indices, col_indices, values);
assert!(matches!(result, Err(CooError::InvalidColumnIndex { .. })));
}
}