From 344591351adf4b47decc426f7c32c2727298be7c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Del=20Pino?= <stephane.delpino44@gmail.com>
Date: Thu, 1 Jul 2021 16:36:32 +0200
Subject: [PATCH] Replace SparseMatrixDescriptor by CRSMatrixDescriptor

The new class is meant to be much faster
- it requires an estimation of the number values per row
- if the number of stored values per line exceeds the reserved one, an
a special storage zone (per row) is used.

It is important to give a precise estimation of the number of values
per rows:
- if the provided expected number of values is too small with regard
to the real number of values stored for a given row, then building
performances can be noticeably affected
- if the provided expected number of values per row corresponds
exactly to the effective number of stored values, then the
construction of the CRSMatrix costs nothing.

In the later case, one benefits from a significant CPU and memory
costs reduction.
---
 src/algebra/CRSMatrix.hpp              |  84 +++++---
 src/algebra/CRSMatrixDescriptor.hpp    | 265 +++++++++++++++++++++++++
 src/algebra/LinearSolver.cpp           |   4 +-
 src/algebra/LinearSolver.hpp           |   2 +-
 src/algebra/PETScUtils.cpp             |  38 ++--
 src/algebra/PETScUtils.hpp             |   7 +-
 src/algebra/SparseMatrixDescriptor.hpp | 170 ----------------
 tests/CMakeLists.txt                   |   2 +-
 tests/test_BiCGStab.cpp                |  17 +-
 tests/test_CG.cpp                      |  23 ++-
 tests/test_CRSMatrix.cpp               | 118 ++++++++---
 tests/test_CRSMatrixDescriptor.cpp     | 166 ++++++++++++++++
 tests/test_EigenvalueSolver.cpp        |   9 +-
 tests/test_LinearSolver.cpp            |  18 +-
 tests/test_SparseMatrixDescriptor.cpp  | 201 -------------------
 15 files changed, 640 insertions(+), 484 deletions(-)
 create mode 100644 src/algebra/CRSMatrixDescriptor.hpp
 delete mode 100644 src/algebra/SparseMatrixDescriptor.hpp
 create mode 100644 tests/test_CRSMatrixDescriptor.cpp
 delete mode 100644 tests/test_SparseMatrixDescriptor.cpp

diff --git a/src/algebra/CRSMatrix.hpp b/src/algebra/CRSMatrix.hpp
index edb85d5e8..bf650afe2 100644
--- a/src/algebra/CRSMatrix.hpp
+++ b/src/algebra/CRSMatrix.hpp
@@ -1,28 +1,31 @@
 #ifndef CRS_MATRIX_HPP
 #define CRS_MATRIX_HPP
 
-#include <Kokkos_StaticCrsGraph.hpp>
-
 #include <utils/Array.hpp>
 #include <utils/PugsAssert.hpp>
 
-#include <algebra/SparseMatrixDescriptor.hpp>
 #include <algebra/Vector.hpp>
 
 #include <iostream>
 
-template <typename DataType = double, typename IndexType = size_t>
+template <typename DataType, typename IndexType>
+class CRSMatrixDescriptor;
+
+template <typename DataType = double, typename IndexType = int>
 class CRSMatrix
 {
+ public:
   using MutableDataType = std::remove_const_t<DataType>;
+  using index_type      = IndexType;
+  using data_type       = DataType;
 
  private:
-  using HostMatrix = Kokkos::StaticCrsGraph<const IndexType, Kokkos::HostSpace>;
+  const IndexType m_nb_rows;
+  const IndexType m_nb_columns;
 
-  HostMatrix m_host_matrix;
+  Array<const IndexType> m_row_map;
   Array<const DataType> m_values;
-  size_t m_nb_rows;
-  size_t m_nb_columns;
+  Array<const IndexType> m_column_indices;
 
  public:
   PUGS_INLINE
@@ -33,14 +36,14 @@ class CRSMatrix
   }
 
   PUGS_INLINE
-  size_t
+  IndexType
   numberOfRows() const
   {
     return m_nb_rows;
   }
 
   PUGS_INLINE
-  size_t
+  IndexType
   numberOfColumns() const
   {
     return m_nb_columns;
@@ -53,15 +56,15 @@ class CRSMatrix
   }
 
   auto
-  rowIndices() const
+  rowMap() const
   {
-    return encapsulate(m_host_matrix.row_map);
+    return m_row_map;
   }
 
   auto
-  row(size_t i) const
+  columnIndices() const
   {
-    return m_host_matrix.rowConst(i);
+    return m_column_indices;
   }
 
   template <typename DataType2>
@@ -71,18 +74,17 @@ class CRSMatrix
     static_assert(std::is_same<std::remove_const_t<DataType>, std::remove_const_t<DataType2>>(),
                   "Cannot multiply matrix and vector of different type");
 
-    Assert(x.size() == m_host_matrix.numRows());
+    Assert(x.size() - m_nb_columns == 0);
 
-    Vector<MutableDataType> Ax{m_host_matrix.numRows()};
-    auto host_row_map = m_host_matrix.row_map;
+    Vector<MutableDataType> Ax(m_nb_rows);
 
     parallel_for(
-      m_host_matrix.numRows(), PUGS_LAMBDA(const IndexType& i_row) {
-        const auto row_begin = host_row_map(i_row);
-        const auto row_end   = host_row_map(i_row + 1);
+      m_nb_rows, PUGS_LAMBDA(const IndexType& i_row) {
+        const auto row_begin = m_row_map[i_row];
+        const auto row_end   = m_row_map[i_row + 1];
         MutableDataType sum{0};
         for (IndexType j = row_begin; j < row_end; ++j) {
-          sum += m_values[j] * x[m_host_matrix.entries(j)];
+          sum += m_values[j] * x[m_column_indices[j]];
         }
         Ax[i_row] = sum;
       });
@@ -90,21 +92,37 @@ class CRSMatrix
     return Ax;
   }
 
-  CRSMatrix(const SparseMatrixDescriptor<DataType, IndexType>& M)
-    : m_nb_rows{M.numberOfRows()}, m_nb_columns{M.numberOfColumns()}
+  friend PUGS_INLINE std::ostream&
+  operator<<(std::ostream& os, const CRSMatrix& A)
   {
-    {
-      auto host_matrix =
-        Kokkos::create_staticcrsgraph<Kokkos::StaticCrsGraph<IndexType, Kokkos::HostSpace>>("connectivity_matrix",
-                                                                                            M.graphVector());
-
-      // This is a bit crappy but it is the price to pay to avoid
-      m_host_matrix.entries           = host_matrix.entries;
-      m_host_matrix.row_map           = host_matrix.row_map;
-      m_host_matrix.row_block_offsets = host_matrix.row_block_offsets;
+    for (IndexType i = 0; i < A.m_nb_rows; ++i) {
+      const auto row_begin = A.m_row_map[i];
+      const auto row_end   = A.m_row_map[i + 1];
+      os << i << "|";
+      for (IndexType j = row_begin; j < row_end; ++j) {
+        os << ' ' << A.m_column_indices[j] << ':' << A.m_values[j];
+      }
+      os << '\n';
     }
-    m_values = M.valueArray();
+    return os;
   }
+
+ private:
+  friend class CRSMatrixDescriptor<DataType, IndexType>;
+
+  CRSMatrix(const IndexType nb_rows,
+            const IndexType nb_columns,
+            const Array<const IndexType>& row_map,
+            const Array<const DataType>& values,
+            const Array<const IndexType>& column_indices)
+    : m_nb_rows{nb_rows},
+      m_nb_columns{nb_columns},
+      m_row_map{row_map},
+      m_values{values},
+      m_column_indices{column_indices}
+  {}
+
+ public:
   ~CRSMatrix() = default;
 };
 
diff --git a/src/algebra/CRSMatrixDescriptor.hpp b/src/algebra/CRSMatrixDescriptor.hpp
new file mode 100644
index 000000000..cbf82a904
--- /dev/null
+++ b/src/algebra/CRSMatrixDescriptor.hpp
@@ -0,0 +1,265 @@
+#ifndef CRS_MATRIX_DESCRIPTOR_HPP
+#define CRS_MATRIX_DESCRIPTOR_HPP
+
+#include <algebra/CRSMatrix.hpp>
+#include <utils/Array.hpp>
+#include <utils/Exceptions.hpp>
+
+#include <map>
+
+template <typename DataType = double, typename IndexType = int>
+class CRSMatrixDescriptor
+{
+ public:
+  using index_type = IndexType;
+  using data_type  = DataType;
+
+  static_assert(std::is_integral_v<IndexType>);
+
+ private:
+  const IndexType m_nb_rows;
+  const IndexType m_nb_columns;
+
+  Array<const IndexType> m_row_map;
+  Array<data_type> m_values;
+  Array<IndexType> m_column_indices;
+
+  Array<IndexType> m_nb_values_per_row;
+  Array<std::map<IndexType, DataType>> m_overflow_per_row;
+
+  Array<const IndexType>
+  _computeRowMap(const Array<IndexType>& non_zeros) const
+  {
+    Assert(non_zeros.size() - m_nb_rows == 0);
+
+    Array<IndexType> row_map(m_nb_rows + 1);
+    row_map[0] = 0;
+    for (IndexType i = 0; i < m_nb_rows; ++i) {
+      row_map[i + 1] = row_map[i] + non_zeros[i];
+    }
+
+    return row_map;
+  }
+
+ public:
+  IndexType
+  numberOfRows() const
+  {
+    return m_nb_rows;
+  }
+
+  IndexType
+  numberOfColumns() const
+  {
+    return m_nb_columns;
+  }
+
+  bool PUGS_INLINE
+  hasOverflow() const
+  {
+    for (IndexType i = 0; i < m_nb_rows; ++i) {
+      if (m_overflow_per_row[i].size() > 0) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  bool PUGS_INLINE
+  isFilled() const
+  {
+    for (IndexType i = 0; i < m_nb_rows; ++i) {
+      if (m_nb_values_per_row[i] < m_row_map[i + 1] - m_row_map[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  friend PUGS_INLINE std::ostream&
+  operator<<(std::ostream& os, const CRSMatrixDescriptor& A)
+  {
+    for (IndexType i = 0; i < A.m_nb_rows; ++i) {
+      const auto& overflow_row = A.m_overflow_per_row[i];
+      os << i << "|";
+
+      if (A.m_nb_values_per_row[i] > 0) {
+        IndexType j     = 0;
+        auto i_overflow = overflow_row.begin();
+        while (j < A.m_nb_values_per_row[i] or i_overflow != overflow_row.end()) {
+          const IndexType j_index = [&] {
+            if (j < A.m_nb_values_per_row[i]) {
+              return A.m_column_indices[A.m_row_map[i] + j];
+            } else {
+              return std::numeric_limits<IndexType>::max();
+            }
+          }();
+
+          const IndexType overflow_index = [&] {
+            if (i_overflow != overflow_row.end()) {
+              return i_overflow->first;
+            } else {
+              return std::numeric_limits<IndexType>::max();
+            }
+          }();
+
+          if (j_index < overflow_index) {
+            os << ' ' << A.m_column_indices[A.m_row_map[i] + j] << ':' << A.m_values[A.m_row_map[i] + j];
+            ++j;
+          } else {
+            os << ' ' << i_overflow->first << ':' << i_overflow->second;
+            ++i_overflow;
+          }
+        }
+      }
+
+      os << '\n';
+    }
+    return os;
+  }
+
+  [[nodiscard]] PUGS_INLINE DataType&
+  operator()(const IndexType i, const IndexType j)
+  {
+    Assert(i < m_nb_rows, "invalid row index");
+    Assert(j < m_nb_columns, "invalid column index");
+
+    const IndexType row_start = m_row_map[i];
+    const IndexType row_end   = m_row_map[i + 1];
+
+    IndexType position_candidate = 0;
+    bool found                   = false;
+    for (IndexType i_row = 0; i_row < m_nb_values_per_row[i]; ++i_row) {
+      if (m_column_indices[row_start + i_row] > j) {
+        position_candidate = i_row;
+        break;
+      } else {
+        if (m_column_indices[row_start + i_row] == j) {
+          position_candidate = i_row;
+          found              = true;
+          break;
+        } else {
+          ++position_candidate;
+        }
+      }
+    }
+
+    if (not found) {
+      if (m_nb_values_per_row[i] < row_end - row_start) {
+        for (IndexType i_shift = 0; i_shift < m_nb_values_per_row[i] - position_candidate; ++i_shift) {
+          Assert(std::make_signed_t<IndexType>(row_start + m_nb_values_per_row[i]) -
+                   std::make_signed_t<IndexType>(i_shift + 1) >=
+                 0);
+          const IndexType i_destination = row_start + m_nb_values_per_row[i] - i_shift;
+          const IndexType i_source      = i_destination - 1;
+
+          m_column_indices[i_destination] = m_column_indices[i_source];
+          m_values[i_destination]         = m_values[i_source];
+        }
+
+        m_column_indices[row_start + position_candidate] = j;
+        m_values[row_start + position_candidate]         = 0;
+        ++m_nb_values_per_row[i];
+
+        return m_values[row_start + position_candidate];
+      } else {
+        auto& overflow_row = m_overflow_per_row[i];
+
+        auto iterator = overflow_row.insert(std::make_pair(j, DataType{0})).first;
+
+        return iterator->second;
+      }
+    } else {
+      return m_values[row_start + position_candidate];
+    }
+  }
+
+  CRSMatrix<DataType, IndexType>
+  getCRSMatrix() const
+  {
+    const bool is_filled    = this->isFilled();
+    const bool has_overflow = this->hasOverflow();
+    if (is_filled and not has_overflow) {
+      return CRSMatrix<DataType, IndexType>{m_nb_rows, m_nb_columns, m_row_map, m_values, m_column_indices};
+    } else {
+      std::cout << rang::fgB::yellow << "warning:" << rang::style::reset
+                << " CRSMatrix profile was not properly set:\n";
+      if (not is_filled) {
+        std::cout << "- some rows are " << rang::style::bold << "too long" << rang::style::reset << '\n';
+      }
+      if (has_overflow) {
+        std::cout << "- some rows are " << rang::style::bold << "too short" << rang::style::reset << '\n';
+      }
+
+      Array<IndexType> row_map(m_nb_rows + 1);
+
+      row_map[0] = 0;
+      for (IndexType i = 0; i < m_nb_rows; ++i) {
+        row_map[i + 1] = row_map[i] + m_nb_values_per_row[i] + m_overflow_per_row[i].size();
+      }
+
+      IndexType nb_values = row_map[row_map.size() - 1];
+
+      Array<data_type> values(nb_values);
+      Array<IndexType> column_indices(nb_values);
+
+      IndexType l = 0;
+      for (IndexType i = 0; i < m_nb_rows; ++i) {
+        const auto& overflow_row = m_overflow_per_row[i];
+
+        if (m_nb_values_per_row[i] > 0) {
+          IndexType j     = 0;
+          auto i_overflow = overflow_row.begin();
+          while (j < m_nb_values_per_row[i] or i_overflow != overflow_row.end()) {
+            const IndexType j_index = [&] {
+              if (j < m_nb_values_per_row[i]) {
+                return m_column_indices[m_row_map[i] + j];
+              } else {
+                return std::numeric_limits<IndexType>::max();
+              }
+            }();
+
+            const IndexType overflow_index = [&] {
+              if (i_overflow != overflow_row.end()) {
+                return i_overflow->first;
+              } else {
+                return std::numeric_limits<IndexType>::max();
+              }
+            }();
+
+            if (j_index < overflow_index) {
+              column_indices[l] = m_column_indices[m_row_map[i] + j];
+              values[l]         = m_values[m_row_map[i] + j];
+              ++j;
+
+            } else {
+              column_indices[l] = i_overflow->first;
+              values[l]         = i_overflow->second;
+              ++i_overflow;
+            }
+            ++l;
+          }
+        }
+      }
+
+      return CRSMatrix<DataType, IndexType>{m_nb_rows, m_nb_columns, row_map, values, column_indices};
+    }
+  }
+
+  CRSMatrixDescriptor(const IndexType nb_rows, const IndexType nb_columns, const Array<IndexType>& non_zeros)
+    : m_nb_rows{nb_rows},
+      m_nb_columns{nb_columns},
+      m_row_map{this->_computeRowMap(non_zeros)},
+      m_values(m_row_map[m_row_map.size() - 1]),
+      m_column_indices(m_row_map[m_row_map.size() - 1]),
+      m_nb_values_per_row(m_nb_rows),
+      m_overflow_per_row(m_nb_rows)
+  {
+    m_nb_values_per_row.fill(0);
+    m_values.fill(0);
+  }
+
+  ~CRSMatrixDescriptor() = default;
+};
+
+#endif   // CRS_MATRIX_DESCRIPTOR_HPP
diff --git a/src/algebra/LinearSolver.cpp b/src/algebra/LinearSolver.cpp
index 98e2a3a1c..3cffca729 100644
--- a/src/algebra/LinearSolver.cpp
+++ b/src/algebra/LinearSolver.cpp
@@ -175,7 +175,7 @@ struct LinearSolver::Internals
                         const RHSVectorType& b,
                         const LinearSolverOptions& options)
   {
-    Assert(x.size() == b.size() and x.size() == A.numberOfColumns() and A.isSquare());
+    Assert((x.size() == b.size()) and (x.size() - A.numberOfColumns() == 0) and A.isSquare());
 
     Vec petscB;
     VecCreateMPIWithArray(PETSC_COMM_SELF, 1, b.size(), b.size(), &b[0], &petscB);
@@ -329,7 +329,7 @@ LinearSolver::checkOptions(const LinearSolverOptions& options) const
 }
 
 void
-LinearSolver::solveLocalSystem(const CRSMatrix<double, size_t>& A, Vector<double>& x, const Vector<const double>& b)
+LinearSolver::solveLocalSystem(const CRSMatrix<double, int>& A, Vector<double>& x, const Vector<const double>& b)
 {
   Internals::solveLocalSystem(A, x, b, m_options);
 }
diff --git a/src/algebra/LinearSolver.hpp b/src/algebra/LinearSolver.hpp
index 387337400..1ab43a140 100644
--- a/src/algebra/LinearSolver.hpp
+++ b/src/algebra/LinearSolver.hpp
@@ -39,7 +39,7 @@ class LinearSolver
     }
   }
 
-  void solveLocalSystem(const CRSMatrix<double, size_t>& A, Vector<double>& x, const Vector<const double>& b);
+  void solveLocalSystem(const CRSMatrix<double, int>& A, Vector<double>& x, const Vector<const double>& b);
   void solveLocalSystem(const DenseMatrix<double>& A, Vector<double>& x, const Vector<const double>& b);
 
   LinearSolver();
diff --git a/src/algebra/PETScUtils.cpp b/src/algebra/PETScUtils.cpp
index 5ed261353..63c5a68ac 100644
--- a/src/algebra/PETScUtils.cpp
+++ b/src/algebra/PETScUtils.cpp
@@ -35,32 +35,22 @@ PETScAijMatrixEmbedder::PETScAijMatrixEmbedder(const size_t nb_rows, const size_
   MatAssemblyEnd(m_petscMat, MAT_FINAL_ASSEMBLY);
 }
 
-PETScAijMatrixEmbedder::PETScAijMatrixEmbedder(const CRSMatrix<double, size_t>& A)
-  : m_nb_rows{A.numberOfRows()}, m_nb_columns{A.numberOfColumns()}
+PETScAijMatrixEmbedder::PETScAijMatrixEmbedder(const CRSMatrix<double, int>& A)
+  : m_nb_rows(A.numberOfRows()), m_nb_columns(A.numberOfColumns())
 {
-  const Array<PetscReal>& values = copy(A.values());
+  static_assert(std::is_same_v<PetscInt, typename std::decay_t<decltype(A)>::index_type>,
+                "index type conversion is not implemented yet");
+  static_assert(std::is_same_v<PetscScalar, typename std::decay_t<decltype(A)>::data_type>,
+                "value type conversion is not implemented yet");
+  m_values         = copy(A.values());
+  m_row_indices    = copy(A.rowMap());
+  m_column_indices = copy(A.columnIndices());
 
-  {
-    const auto& A_row_indices = A.rowIndices();
-    Array<PetscInt> row_indices{A_row_indices.size()};
-    for (size_t i = 0; i < row_indices.size(); ++i) {
-      row_indices[i] = A_row_indices[i];
-    }
-    m_row_indices = row_indices;
-
-    Array<PetscInt> column_indices{values.size()};
-    size_t l = 0;
-    for (size_t i = 0; i < A.numberOfRows(); ++i) {
-      const auto row_i = A.row(i);
-      for (size_t j = 0; j < row_i.length; ++j) {
-        column_indices[l++] = row_i.colidx(j);
-      }
-    }
-    m_column_indices = column_indices;
-  }
-
-  MatCreateSeqAIJWithArrays(PETSC_COMM_SELF, A.numberOfRows(), A.numberOfColumns(), &m_row_indices[0],
-                            &m_column_indices[0], &values[0], &m_petscMat);
+  MatCreateSeqAIJWithArrays(PETSC_COMM_SELF, A.numberOfRows(), A.numberOfColumns(),
+                            const_cast<PetscInt*>(&m_row_indices[0]),      // These ugly const_cast
+                            const_cast<PetscInt*>(&m_column_indices[0]),   // permit CRSMatrix to
+                            const_cast<PetscScalar*>(&m_values[0]),        // be non-mutable
+                            &m_petscMat);
 
   MatAssemblyBegin(m_petscMat, MAT_FINAL_ASSEMBLY);
   MatAssemblyEnd(m_petscMat, MAT_FINAL_ASSEMBLY);
diff --git a/src/algebra/PETScUtils.hpp b/src/algebra/PETScUtils.hpp
index 68748c02f..3e7cff5b8 100644
--- a/src/algebra/PETScUtils.hpp
+++ b/src/algebra/PETScUtils.hpp
@@ -15,8 +15,9 @@ class PETScAijMatrixEmbedder
 {
  private:
   Mat m_petscMat;
-  Array<PetscInt> m_row_indices;
-  Array<PetscInt> m_column_indices;
+  Array<const PetscScalar> m_values;
+  Array<const PetscInt> m_row_indices;
+  Array<const PetscInt> m_column_indices;
   const size_t m_nb_rows;
   const size_t m_nb_columns;
 
@@ -57,7 +58,7 @@ class PETScAijMatrixEmbedder
     : PETScAijMatrixEmbedder{A.numberOfRows(), A.numberOfColumns(), &A(0, 0)}
   {}
 
-  PETScAijMatrixEmbedder(const CRSMatrix<double, size_t>& A);
+  PETScAijMatrixEmbedder(const CRSMatrix<double, int>& A);
 
   ~PETScAijMatrixEmbedder();
 };
diff --git a/src/algebra/SparseMatrixDescriptor.hpp b/src/algebra/SparseMatrixDescriptor.hpp
deleted file mode 100644
index d94ed4686..000000000
--- a/src/algebra/SparseMatrixDescriptor.hpp
+++ /dev/null
@@ -1,170 +0,0 @@
-#ifndef SPARSE_MATRIX_DESCRIPTOR_HPP
-#define SPARSE_MATRIX_DESCRIPTOR_HPP
-
-#include <utils/Array.hpp>
-
-#include <map>
-#include <type_traits>
-
-template <typename DataType = double, typename IndexType = size_t>
-class SparseMatrixDescriptor
-{
-  static_assert(std::is_integral_v<IndexType>, "Index type must be an integral type");
-  static_assert(std::is_unsigned_v<IndexType>, "Index type must be unsigned");
-
- public:
-  using data_type  = DataType;
-  using index_type = IndexType;
-
-  class SparseRowDescriptor
-  {
-    friend class SparseMatrixDescriptor;
-    std::map<IndexType, DataType> m_id_value_map;
-
-   public:
-    IndexType
-    numberOfValues() const noexcept
-    {
-      return m_id_value_map.size();
-    }
-
-    const DataType&
-    operator[](const IndexType& j) const
-    {
-      auto i_value = m_id_value_map.find(j);
-      Assert(i_value != m_id_value_map.end());
-      return i_value->second;
-    }
-
-    DataType&
-    operator[](const IndexType& j)
-    {
-      auto i_value = m_id_value_map.find(j);
-      if (i_value != m_id_value_map.end()) {
-        return i_value->second;
-      } else {
-        auto [i_inserted, success] = m_id_value_map.insert(std::make_pair(j, 0));
-        Assert(success);
-        return i_inserted->second;
-      }
-    }
-
-    friend std::ostream&
-    operator<<(std::ostream& os, const SparseRowDescriptor& row)
-    {
-      for (auto [j, value] : row.m_id_value_map) {
-        os << ' ' << static_cast<size_t>(j) << ':' << value;
-      }
-      return os;
-    }
-
-    SparseRowDescriptor() = default;
-  };
-
- private:
-  Array<SparseRowDescriptor> m_row_array;
-  const size_t m_nb_columns;
-
- public:
-  PUGS_INLINE
-  size_t
-  numberOfRows() const noexcept
-  {
-    return m_row_array.size();
-  }
-
-  PUGS_INLINE
-  size_t
-  numberOfColumns() const noexcept
-  {
-    return m_nb_columns;
-  }
-
-  SparseRowDescriptor&
-  row(const IndexType i)
-  {
-    Assert(i < m_row_array.size());
-    return m_row_array[i];
-  }
-
-  const SparseRowDescriptor&
-  row(const IndexType i) const
-  {
-    Assert(i < m_row_array.size());
-    return m_row_array[i];
-  }
-
-  DataType&
-  operator()(const IndexType& i, const IndexType& j)
-  {
-    Assert(i < m_row_array.size());
-    Assert(j < m_nb_columns);
-    return m_row_array[i][j];
-  }
-
-  const DataType&
-  operator()(const IndexType& i, const IndexType& j) const
-  {
-    Assert(i < m_row_array.size());
-    Assert(j < m_nb_columns);
-    const auto& r = m_row_array[i];   // split to ensure const-ness of call
-    return r[j];
-  }
-
-  friend std::ostream&
-  operator<<(std::ostream& os, const SparseMatrixDescriptor& M)
-  {
-    for (IndexType i = 0; i < M.m_row_array.size(); ++i) {
-      os << static_cast<size_t>(i) << " |" << M.m_row_array[i] << '\n';
-    }
-    return os;
-  }
-
-  auto
-  graphVector() const
-  {
-    std::vector<std::vector<IndexType>> graph_vector(m_row_array.size());
-    for (IndexType i_row = 0; i_row < m_row_array.size(); ++i_row) {
-      const SparseRowDescriptor& row = m_row_array[i_row];
-      for (auto [id, value] : row.m_id_value_map) {
-        graph_vector[i_row].push_back(id);
-      }
-    }
-    return graph_vector;
-  }
-
-  Array<DataType>
-  valueArray() const
-  {
-    IndexType size = 0;
-    for (IndexType i_row = 0; i_row < m_row_array.size(); ++i_row) {
-      size += m_row_array[i_row].m_id_value_map.size();
-    }
-
-    Array<DataType> values(size);
-
-    IndexType cpt = 0;
-    for (IndexType i_row = 0; i_row < m_row_array.size(); ++i_row) {
-      const SparseRowDescriptor& row = m_row_array[i_row];
-      for (auto [id, value] : row.m_id_value_map) {
-        values[cpt++] = value;
-      }
-    }
-    return values;
-  }
-
-  SparseMatrixDescriptor(size_t nb_rows, size_t nb_columns) : m_row_array{nb_rows}, m_nb_columns{nb_columns}
-  {
-    if (nb_rows == nb_columns) {
-      for (size_t i = 0; i < nb_rows; ++i) {
-        m_row_array[i][i] = 0;
-      }
-    }
-  }
-
-  SparseMatrixDescriptor(size_t nb_rows) : SparseMatrixDescriptor{nb_rows, nb_rows} {}
-
-  ~SparseMatrixDescriptor() = default;
-};
-
-#endif   // SPARSE_MATRIX_DESCRIPTOR_HPP
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index a56fb1f9d..7904eedc0 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -56,6 +56,7 @@ add_executable (unit_tests
   test_ConcatExpressionProcessor.cpp
   test_CRSGraph.cpp
   test_CRSMatrix.cpp
+  test_CRSMatrixDescriptor.cpp
   test_DataVariant.cpp
   test_DenseMatrix.cpp
   test_Demangle.cpp
@@ -89,7 +90,6 @@ add_executable (unit_tests
   test_PugsFunctionAdapter.cpp
   test_PugsUtils.cpp
   test_RevisionInfo.cpp
-  test_SparseMatrixDescriptor.cpp
   test_SymbolTable.cpp
   test_Table.cpp
   test_Timer.cpp
diff --git a/tests/test_BiCGStab.cpp b/tests/test_BiCGStab.cpp
index e1be80698..231783090 100644
--- a/tests/test_BiCGStab.cpp
+++ b/tests/test_BiCGStab.cpp
@@ -3,6 +3,7 @@
 
 #include <algebra/BiCGStab.hpp>
 #include <algebra/CRSMatrix.hpp>
+#include <algebra/CRSMatrixDescriptor.hpp>
 
 // clazy:excludeall=non-pod-global-static
 
@@ -10,7 +11,13 @@ TEST_CASE("BiCGStab", "[algebra]")
 {
   SECTION("no preconditionner")
   {
-    SparseMatrixDescriptor S{5};
+    Array<int> non_zeros(5);
+    non_zeros[0] = 2;
+    non_zeros[1] = 3;
+    non_zeros[2] = 3;
+    non_zeros[3] = 3;
+    non_zeros[4] = 2;
+    CRSMatrixDescriptor<double> S{5, 5, non_zeros};
     S(0, 0) = 2;
     S(0, 1) = -1;
 
@@ -29,7 +36,7 @@ TEST_CASE("BiCGStab", "[algebra]")
     S(4, 3) = 1;
     S(4, 4) = 3;
 
-    CRSMatrix A{S};
+    CRSMatrix A{S.getCRSMatrix()};
 
     Vector<const double> x_exact = [] {
       Vector<double> y{5};
@@ -53,7 +60,9 @@ TEST_CASE("BiCGStab", "[algebra]")
 
   SECTION("no preconditionner non-converged")
   {
-    SparseMatrixDescriptor S{5};
+    Array<int> non_zeros(5);
+    non_zeros.fill(2);
+    CRSMatrixDescriptor<double> S{5, 5, non_zeros};
     S(0, 0) = 2;
     S(0, 1) = -1;
 
@@ -72,7 +81,7 @@ TEST_CASE("BiCGStab", "[algebra]")
     S(4, 3) = 1;
     S(4, 4) = 3;
 
-    CRSMatrix A{S};
+    CRSMatrix A{S.getCRSMatrix()};
 
     Vector<const double> x_exact = [] {
       Vector<double> y{5};
diff --git a/tests/test_CG.cpp b/tests/test_CG.cpp
index 05919858f..b76d90a04 100644
--- a/tests/test_CG.cpp
+++ b/tests/test_CG.cpp
@@ -3,6 +3,7 @@
 
 #include <algebra/CG.hpp>
 #include <algebra/CRSMatrix.hpp>
+#include <algebra/CRSMatrixDescriptor.hpp>
 
 // clazy:excludeall=non-pod-global-static
 
@@ -10,7 +11,11 @@ TEST_CASE("CG", "[algebra]")
 {
   SECTION("no preconditionner")
   {
-    SparseMatrixDescriptor S{5};
+    Array<int> non_zeros{5};
+    non_zeros.fill(3);
+    non_zeros[0] = 2;
+    non_zeros[4] = 2;
+    CRSMatrixDescriptor S{5, 5, non_zeros};
     S(0, 0) = 2;
     S(0, 1) = -1;
 
@@ -29,7 +34,7 @@ TEST_CASE("CG", "[algebra]")
     S(4, 3) = -1;
     S(4, 4) = 2;
 
-    CRSMatrix A{S};
+    CRSMatrix A{S.getCRSMatrix()};
 
     Vector<const double> x_exact = [] {
       Vector<double> y{5};
@@ -53,9 +58,11 @@ TEST_CASE("CG", "[algebra]")
 
   SECTION("singular matrix with RHS in its image")
   {
-    SparseMatrixDescriptor<double, uint8_t> S{5};
+    Array<int> non_zeros(5);
+    non_zeros.fill(0);
+    CRSMatrixDescriptor<double> S{5, 5, non_zeros};
 
-    CRSMatrix<double, uint8_t> A{S};
+    CRSMatrix<double> A{S.getCRSMatrix()};
 
     Vector<double> b{5};
     b = 0;
@@ -69,7 +76,11 @@ TEST_CASE("CG", "[algebra]")
 
   SECTION("no preconditionner non-converged")
   {
-    SparseMatrixDescriptor S{5};
+    Array<int> non_zeros(5);
+    non_zeros.fill(3);
+    non_zeros[0] = 2;
+    non_zeros[4] = 2;
+    CRSMatrixDescriptor<double> S{5, 5, non_zeros};
     S(0, 0) = 2;
     S(0, 1) = -1;
 
@@ -88,7 +99,7 @@ TEST_CASE("CG", "[algebra]")
     S(4, 3) = -1;
     S(4, 4) = 2;
 
-    CRSMatrix A{S};
+    CRSMatrix A{S.getCRSMatrix()};
 
     Vector<const double> x_exact = [] {
       Vector<double> y{5};
diff --git a/tests/test_CRSMatrix.cpp b/tests/test_CRSMatrix.cpp
index d6bfdaba9..e0aa27b57 100644
--- a/tests/test_CRSMatrix.cpp
+++ b/tests/test_CRSMatrix.cpp
@@ -2,9 +2,7 @@
 #include <catch2/matchers/catch_matchers_all.hpp>
 
 #include <algebra/CRSMatrix.hpp>
-
-// Instantiate to ensure full coverage is performed
-template class SparseMatrixDescriptor<int, uint8_t>;
+#include <algebra/CRSMatrixDescriptor.hpp>
 
 // clazy:excludeall=non-pod-global-static
 
@@ -12,7 +10,10 @@ TEST_CASE("CRSMatrix", "[algebra]")
 {
   SECTION("matrix size")
   {
-    SparseMatrixDescriptor<int, uint8_t> S{5};
+    Array<int> non_zeros{6};
+    non_zeros.fill(3);
+
+    CRSMatrixDescriptor<int> S{6, 5, non_zeros};
     S(0, 2) = 3;
     S(1, 2) = 11;
     S(1, 1) = 1;
@@ -22,16 +23,19 @@ TEST_CASE("CRSMatrix", "[algebra]")
     S(2, 2) = 4;
     S(4, 3) = 2;
     S(4, 4) = -2;
+    S(5, 2) = 1;
 
-    CRSMatrix<int, uint8_t> A{S};
+    CRSMatrix<int> A{S.getCRSMatrix()};
 
     REQUIRE(A.numberOfRows() == S.numberOfRows());
     REQUIRE(A.numberOfColumns() == S.numberOfColumns());
   }
 
-  SECTION("matrix vector product (simple)")
+  SECTION("square matrix vector product (simple)")
   {
-    SparseMatrixDescriptor<int, uint8_t> S{5};
+    Array<int> non_zeros{5};
+    non_zeros.fill(3);
+    CRSMatrixDescriptor<int> S{5, 5, non_zeros};
     S(0, 2) = 3;
     S(1, 2) = 11;
     S(1, 1) = 1;
@@ -42,9 +46,9 @@ TEST_CASE("CRSMatrix", "[algebra]")
     S(4, 3) = 2;
     S(4, 4) = -2;
 
-    CRSMatrix<int, uint8_t> A{S};
+    CRSMatrix<int> A{S.getCRSMatrix()};
 
-    Vector<int> x{A.numberOfColumns()};
+    Vector<int> x(A.numberOfColumns());
     x    = 0;
     x[0] = 1;
 
@@ -96,9 +100,39 @@ TEST_CASE("CRSMatrix", "[algebra]")
     REQUIRE(y[4] == -2);
   }
 
+  SECTION("rectangular matrix vector product (simple)")
+  {
+    Array<int> non_zeros{3};
+    non_zeros.fill(3);
+    CRSMatrixDescriptor<int> S{3, 5, non_zeros};
+    S(0, 2) = 3;
+    S(0, 4) = 2;
+    S(1, 2) = 11;
+    S(1, 1) = 2;
+    S(1, 0) = 1;
+    S(2, 3) = 6;
+    S(2, 2) = 4;
+
+    CRSMatrix<int> A{S.getCRSMatrix()};
+
+    Vector<int> x(A.numberOfColumns());
+    x[0] = 1;
+    x[1] = 3;
+    x[2] = 2;
+    x[3] = -3;
+    x[4] = 5;
+
+    Vector<int> y = A * x;
+    REQUIRE(y[0] == 16);
+    REQUIRE(y[1] == 29);
+    REQUIRE(y[2] == -10);
+  }
+
   SECTION("matrix vector product (complete)")
   {
-    SparseMatrixDescriptor<int, uint8_t> S{4};
+    Array<int> non_zeros{4};
+    non_zeros.fill(4);
+    CRSMatrixDescriptor<int> S{4, 4, non_zeros};
     S(0, 0) = 1;
     S(0, 1) = 2;
     S(0, 2) = 3;
@@ -116,9 +150,9 @@ TEST_CASE("CRSMatrix", "[algebra]")
     S(3, 2) = 15;
     S(3, 3) = 16;
 
-    CRSMatrix<int, uint8_t> A{S};
+    CRSMatrix<int> A{S.getCRSMatrix()};
 
-    Vector<int> x{A.numberOfColumns()};
+    Vector<int> x(A.numberOfColumns());
     x[0] = 1;
     x[1] = 2;
     x[2] = 3;
@@ -133,7 +167,10 @@ TEST_CASE("CRSMatrix", "[algebra]")
 
   SECTION("check values")
   {
-    SparseMatrixDescriptor<int, uint8_t> S{4};
+    Array<int> non_zeros{4};
+    non_zeros.fill(3);
+    non_zeros[2] = 4;
+    CRSMatrixDescriptor<int> S{4, 4, non_zeros};
     S(3, 0) = 13;
     S(0, 0) = 1;
     S(0, 1) = 2;
@@ -148,7 +185,7 @@ TEST_CASE("CRSMatrix", "[algebra]")
     S(1, 0) = 5;
     S(2, 1) = 10;
 
-    CRSMatrix<int, uint8_t> A{S};
+    CRSMatrix<int> A{S.getCRSMatrix()};
 
     auto values = A.values();
     REQUIRE(values.size() == 13);
@@ -166,29 +203,46 @@ TEST_CASE("CRSMatrix", "[algebra]")
     REQUIRE(values[11] == 15);
     REQUIRE(values[12] == 16);
 
-    auto row_indices = A.rowIndices();
-
-    REQUIRE(row_indices.size() == 5);
-
-    REQUIRE(A.row(0).colidx(0) == 0);
-    REQUIRE(A.row(0).colidx(1) == 1);
-    REQUIRE(A.row(0).colidx(2) == 3);
-    REQUIRE(A.row(1).colidx(0) == 0);
-    REQUIRE(A.row(1).colidx(1) == 1);
-    REQUIRE(A.row(1).colidx(2) == 2);
-    REQUIRE(A.row(2).colidx(0) == 0);
-    REQUIRE(A.row(2).colidx(1) == 1);
-    REQUIRE(A.row(2).colidx(2) == 2);
-    REQUIRE(A.row(2).colidx(3) == 3);
-    REQUIRE(A.row(3).colidx(0) == 0);
-    REQUIRE(A.row(3).colidx(1) == 2);
-    REQUIRE(A.row(3).colidx(2) == 3);
+    auto row_map = A.rowMap();
+    REQUIRE(row_map.size() == 5);
+    REQUIRE(row_map[0] == 0);
+    REQUIRE(row_map[1] == 3);
+    REQUIRE(row_map[2] == 6);
+    REQUIRE(row_map[3] == 10);
+    REQUIRE(row_map[4] == 13);
+
+    auto column_indices = A.columnIndices();
+    REQUIRE(column_indices.size() == 13);
+    REQUIRE(column_indices[0] == 0);
+    REQUIRE(column_indices[1] == 1);
+    REQUIRE(column_indices[2] == 3);
+    REQUIRE(column_indices[3] == 0);
+    REQUIRE(column_indices[4] == 1);
+    REQUIRE(column_indices[5] == 2);
+    REQUIRE(column_indices[6] == 0);
+    REQUIRE(column_indices[7] == 1);
+    REQUIRE(column_indices[8] == 2);
+    REQUIRE(column_indices[9] == 3);
+    REQUIRE(column_indices[10] == 0);
+    REQUIRE(column_indices[11] == 2);
+    REQUIRE(column_indices[12] == 3);
+
+    std::ostringstream crs_descriptor_os;
+    std::ostringstream crs_os;
+
+    crs_descriptor_os << S;
+    crs_os << A;
+
+    REQUIRE(crs_descriptor_os.str() == crs_os.str());
   }
 
 #ifndef NDEBUG
   SECTION("incompatible runtime matrix/vector product")
   {
-    CRSMatrix<int, uint8_t> A{SparseMatrixDescriptor<int, uint8_t>{4}};
+    Array<int> non_zeros(2);
+    non_zeros.fill(0);
+    CRSMatrixDescriptor<int> S{2, 4, non_zeros};
+    CRSMatrix<int> A{S.getCRSMatrix()};
     Vector<int> x{2};
     REQUIRE_THROWS_AS(A * x, AssertError);
   }
diff --git a/tests/test_CRSMatrixDescriptor.cpp b/tests/test_CRSMatrixDescriptor.cpp
new file mode 100644
index 000000000..2bf59af22
--- /dev/null
+++ b/tests/test_CRSMatrixDescriptor.cpp
@@ -0,0 +1,166 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <utils/PugsAssert.hpp>
+
+#include <algebra/CRSMatrixDescriptor.hpp>
+
+// Instantiate to ensure full coverage is performed
+template class CRSMatrixDescriptor<int>;
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("CRSMatrixDescriptor", "[algebra]")
+{
+  SECTION("has overflow / not filled")
+  {
+    const size_t nb_lines   = 2;
+    const size_t nb_columns = 5;
+
+    Array<int> non_zeros{nb_lines};
+    non_zeros.fill(2);
+    CRSMatrixDescriptor<int> S(nb_lines, nb_columns, non_zeros);
+
+    S(1, 3) = 5;
+    S(1, 1) += 3;
+    S(0, 1) = 4;
+    S(1, 0) = 2;
+    S(1, 2) = 7;
+
+    REQUIRE(S.numberOfRows() == 2);
+    REQUIRE(S.numberOfColumns() == 5);
+
+    REQUIRE(S.isFilled() == false);
+    REQUIRE(S.hasOverflow() == true);
+
+    std::ostringstream os;
+    os << S;
+
+    std::string_view output =
+      R"(0| 1:4
+1| 0:2 1:3 2:7 3:5
+)";
+    REQUIRE(os.str() == output);
+  }
+
+  SECTION("no overflow / not filled")
+  {
+    const size_t nb_lines   = 2;
+    const size_t nb_columns = 5;
+
+    Array<int> non_zeros{nb_lines};
+    non_zeros.fill(2);
+    CRSMatrixDescriptor<int> S(nb_lines, nb_columns, non_zeros);
+
+    S(1, 3) = 5;
+    S(1, 1) += 3;
+    S(0, 1) = 4;
+
+    REQUIRE(S.numberOfRows() == 2);
+    REQUIRE(S.numberOfColumns() == 5);
+
+    REQUIRE(S.isFilled() == false);
+    REQUIRE(S.hasOverflow() == false);
+
+    std::ostringstream os;
+    os << S;
+
+    std::string_view output =
+      R"(0| 1:4
+1| 1:3 3:5
+)";
+    REQUIRE(os.str() == output);
+  }
+
+  SECTION("no overflow / filled")
+  {
+    const size_t nb_lines   = 2;
+    const size_t nb_columns = 5;
+
+    Array<int> non_zeros{nb_lines};
+    non_zeros[0] = 1;
+    non_zeros[1] = 5;
+    CRSMatrixDescriptor<int> S(nb_lines, nb_columns, non_zeros);
+
+    S(1, 3) = 2;
+    S(1, 3) += 3;
+    S(1, 1) += 3;
+    S(0, 1) = 4;
+    S(1, 2) = 2;
+    S(1, 0) = 1;
+    S(1, 4) = 13;
+
+    REQUIRE(S.numberOfRows() == 2);
+    REQUIRE(S.numberOfColumns() == 5);
+
+    REQUIRE(S.isFilled() == true);
+    REQUIRE(S.hasOverflow() == false);
+
+    std::ostringstream os;
+    os << S;
+
+    std::string_view output =
+      R"(0| 1:4
+1| 0:1 1:3 2:2 3:5 4:13
+)";
+    REQUIRE(os.str() == output);
+  }
+
+  SECTION("has overflow / filled")
+  {
+    const size_t nb_lines   = 2;
+    const size_t nb_columns = 5;
+
+    Array<int> non_zeros{nb_lines};
+    non_zeros.fill(2);
+    CRSMatrixDescriptor<int> S(nb_lines, nb_columns, non_zeros);
+
+    S(1, 3) = 3;
+    S(1, 0) = -2;
+    S(1, 1) += 1;
+    S(1, 1) += 1;
+    S(1, 1) = 3;
+    S(0, 1) = 4;
+    S(0, 0) = 1;
+    S(0, 3) = 5;
+    S(0, 2) = 1;
+
+    REQUIRE(S.numberOfRows() == 2);
+    REQUIRE(S.numberOfColumns() == 5);
+
+    REQUIRE(S.isFilled() == true);
+    REQUIRE(S.hasOverflow() == true);
+
+    std::ostringstream os;
+    os << S;
+
+    std::string_view output =
+      R"(0| 0:1 1:4 2:1 3:5
+1| 0:-2 1:3 3:3
+)";
+    REQUIRE(os.str() == output);
+  }
+
+#ifndef NDEBUG
+  SECTION("incompatible non zero vector size and number of columns")
+  {
+    Array<int> non_zeros(3);
+    REQUIRE_THROWS_AS((CRSMatrixDescriptor<int>{2, 4, non_zeros}), AssertError);
+  }
+
+  SECTION("bad row number")
+  {
+    Array<int> non_zeros(2);
+    CRSMatrixDescriptor<int> S{2, 4, non_zeros};
+    REQUIRE_THROWS_AS(S(2, 3) = 1, AssertError);
+  }
+
+  SECTION("bad column number")
+  {
+    Array<int> non_zeros(2);
+    CRSMatrixDescriptor<int> S{2, 4, non_zeros};
+    REQUIRE_THROWS_AS(S(0, 5) = 1, AssertError);
+  }
+
+#endif   // NDEBUG
+}
diff --git a/tests/test_EigenvalueSolver.cpp b/tests/test_EigenvalueSolver.cpp
index e779bce56..3ca8f7f51 100644
--- a/tests/test_EigenvalueSolver.cpp
+++ b/tests/test_EigenvalueSolver.cpp
@@ -2,9 +2,9 @@
 #include <catch2/matchers/catch_matchers_all.hpp>
 
 #include <algebra/CRSMatrix.hpp>
+#include <algebra/CRSMatrixDescriptor.hpp>
 #include <algebra/DenseMatrix.hpp>
 #include <algebra/EigenvalueSolver.hpp>
-#include <algebra/SparseMatrixDescriptor.hpp>
 
 #include <utils/pugs_config.hpp>
 
@@ -16,7 +16,10 @@ TEST_CASE("EigenvalueSolver", "[algebra]")
   {
     SECTION("symmetric system")
     {
-      SparseMatrixDescriptor S{3};
+      Array<int> non_zeros(3);
+      non_zeros.fill(3);
+      CRSMatrixDescriptor<double> S{3, 3, non_zeros};
+
       S(0, 0) = 3;
       S(0, 1) = 2;
       S(0, 2) = 4;
@@ -29,7 +32,7 @@ TEST_CASE("EigenvalueSolver", "[algebra]")
       S(2, 1) = 2;
       S(2, 2) = 3;
 
-      CRSMatrix A{S};
+      CRSMatrix A{S.getCRSMatrix()};
 
       SECTION("eigenvalues")
       {
diff --git a/tests/test_LinearSolver.cpp b/tests/test_LinearSolver.cpp
index 1219c48e6..a9b4cab1f 100644
--- a/tests/test_LinearSolver.cpp
+++ b/tests/test_LinearSolver.cpp
@@ -3,6 +3,7 @@
 
 #include <utils/pugs_config.hpp>
 
+#include <algebra/CRSMatrixDescriptor.hpp>
 #include <algebra/LinearSolver.hpp>
 
 // clazy:excludeall=non-pod-global-static
@@ -147,7 +148,11 @@ TEST_CASE("LinearSolver", "[algebra]")
     {
       SECTION("symmetric system")
       {
-        SparseMatrixDescriptor S{5};
+        Array<int> non_zeros{5};
+        non_zeros.fill(3);
+        non_zeros[0] = 2;
+        non_zeros[4] = 2;
+        CRSMatrixDescriptor S{5, 5, non_zeros};
         S(0, 0) = 2;
         S(0, 1) = -1;
 
@@ -166,7 +171,7 @@ TEST_CASE("LinearSolver", "[algebra]")
         S(4, 3) = -1;
         S(4, 4) = 2;
 
-        CRSMatrix A{S};
+        CRSMatrix A{S.getCRSMatrix()};
 
         Vector<const double> x_exact = [] {
           Vector<double> y{5};
@@ -302,7 +307,12 @@ TEST_CASE("LinearSolver", "[algebra]")
 
       SECTION("none symmetric system")
       {
-        SparseMatrixDescriptor S{5};
+        Array<int> non_zeros{5};
+        non_zeros.fill(3);
+        non_zeros[0] = 2;
+        non_zeros[4] = 2;
+        CRSMatrixDescriptor S{5, 5, non_zeros};
+
         S(0, 0) = 2;
         S(0, 1) = -1;
 
@@ -321,7 +331,7 @@ TEST_CASE("LinearSolver", "[algebra]")
         S(4, 3) = 1;
         S(4, 4) = 3;
 
-        CRSMatrix A{S};
+        CRSMatrix A{S.getCRSMatrix()};
 
         Vector<const double> x_exact = [] {
           Vector<double> y{5};
diff --git a/tests/test_SparseMatrixDescriptor.cpp b/tests/test_SparseMatrixDescriptor.cpp
deleted file mode 100644
index 77bc890f7..000000000
--- a/tests/test_SparseMatrixDescriptor.cpp
+++ /dev/null
@@ -1,201 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-#include <catch2/matchers/catch_matchers_all.hpp>
-
-#include <utils/PugsAssert.hpp>
-
-#include <algebra/SparseMatrixDescriptor.hpp>
-
-// Instantiate to ensure full coverage is performed
-template class SparseMatrixDescriptor<int, uint8_t>;
-
-// clazy:excludeall=non-pod-global-static
-
-TEST_CASE("SparseMatrixDescriptor", "[algebra]")
-{
-  SECTION("SparseRowDescriptor subclass")
-  {
-    SECTION("number of values")
-    {
-      SparseMatrixDescriptor<int, uint8_t>::SparseRowDescriptor r;
-      REQUIRE(r.numberOfValues() == 0);
-
-      r[0] = 3;
-      r[0] += 2;
-
-      REQUIRE(r.numberOfValues() == 1);
-
-      r[1] = -2;
-
-      REQUIRE(r.numberOfValues() == 2);
-    }
-
-    SECTION("values access")
-    {
-      SparseMatrixDescriptor<int, uint8_t>::SparseRowDescriptor r;
-
-      r[0] = 3;
-      r[0] += 2;
-
-      r[3] = 8;
-
-      REQUIRE(r[0] == 5);
-      REQUIRE(r[3] == 8);
-
-      const auto& const_r = r;
-      REQUIRE(const_r[0] == 5);
-      REQUIRE(const_r[3] == 8);
-
-#ifndef NDEBUG
-      REQUIRE_THROWS_AS(const_r[2], AssertError);
-#endif   // NDEBUG
-    }
-  }
-
-  SECTION("matrix size")
-  {
-    SparseMatrixDescriptor<int, uint8_t> S{5};
-    REQUIRE(S.numberOfRows() == 5);
-    REQUIRE(S.numberOfColumns() == 5);
-  }
-
-  SECTION("location operators")
-  {
-    SparseMatrixDescriptor<int, uint8_t> S{5};
-    S(0, 2) = 3;
-    S(1, 1) = 1;
-    S(1, 2) = 11;
-    S(0, 2) += 2;
-    S(2, 2) = 4;
-    S(3, 1) = 5;
-    S(4, 1) = 1;
-    S(4, 4) = -2;
-
-    REQUIRE(S.row(0).numberOfValues() == 2);
-    REQUIRE(S.row(1).numberOfValues() == 2);
-    REQUIRE(S.row(2).numberOfValues() == 1);
-    REQUIRE(S.row(3).numberOfValues() == 2);
-    REQUIRE(S.row(4).numberOfValues() == 2);
-
-    const auto& const_S = S;
-
-    REQUIRE(const_S.row(0).numberOfValues() == 2);
-    REQUIRE(const_S.row(1).numberOfValues() == 2);
-    REQUIRE(const_S.row(2).numberOfValues() == 1);
-    REQUIRE(const_S.row(3).numberOfValues() == 2);
-    REQUIRE(const_S.row(4).numberOfValues() == 2);
-
-#ifndef NDEBUG
-    REQUIRE_THROWS_AS(S.row(5), AssertError);
-    REQUIRE_THROWS_AS(const_S.row(5), AssertError);
-#endif   // NDEBUG
-
-    REQUIRE(S(0, 2) == 5);
-    REQUIRE(S(1, 1) == 1);
-    REQUIRE(S(1, 2) == 11);
-    REQUIRE(S(2, 2) == 4);
-    REQUIRE(S(3, 1) == 5);
-    REQUIRE(S(4, 1) == 1);
-    REQUIRE(S(4, 4) == -2);
-
-    REQUIRE(const_S(0, 2) == 5);
-    REQUIRE(const_S(1, 1) == 1);
-    REQUIRE(const_S(1, 2) == 11);
-    REQUIRE(const_S(2, 2) == 4);
-    REQUIRE(const_S(3, 1) == 5);
-    REQUIRE(const_S(4, 1) == 1);
-    REQUIRE(const_S(4, 4) == -2);
-
-#ifndef NDEBUG
-    REQUIRE_THROWS_AS(S(5, 0), AssertError);
-    REQUIRE_THROWS_AS(const_S(6, 1), AssertError);
-    REQUIRE_THROWS_AS(const_S(0, 1), AssertError);
-#endif   // NDEBUG
-  }
-
-  SECTION("vector-graph")
-  {
-    SparseMatrixDescriptor<int, uint8_t> S{5};
-    S(0, 2) = 3;
-    S(1, 2) = 11;
-    S(1, 1) = 1;
-    S(0, 2) += 2;
-    S(3, 3) = 5;
-    S(3, 1) = 5;
-    S(4, 1) = 1;
-    S(2, 2) = 4;
-    S(4, 4) = -2;
-
-    const auto graph = S.graphVector();
-
-    REQUIRE(graph.size() == S.numberOfRows());
-    REQUIRE(graph[0].size() == 2);
-    REQUIRE(graph[1].size() == 2);
-    REQUIRE(graph[2].size() == 1);
-    REQUIRE(graph[3].size() == 2);
-    REQUIRE(graph[4].size() == 2);
-
-    REQUIRE(graph[0][0] == 0);
-    REQUIRE(graph[0][1] == 2);
-    REQUIRE(graph[1][0] == 1);
-    REQUIRE(graph[1][1] == 2);
-    REQUIRE(graph[2][0] == 2);
-    REQUIRE(graph[3][0] == 1);
-    REQUIRE(graph[3][1] == 3);
-    REQUIRE(graph[4][0] == 1);
-    REQUIRE(graph[4][1] == 4);
-  }
-
-  SECTION("value array")
-  {
-    SparseMatrixDescriptor<int, uint8_t> S{5};
-    S(0, 2) = 3;
-    S(1, 2) = 11;
-    S(1, 1) = 1;
-    S(0, 2) += 2;
-    S(3, 3) = 5;
-    S(3, 1) = -3;
-    S(4, 1) = 1;
-    S(2, 2) = 4;
-    S(4, 4) = -2;
-
-    const auto value_array = S.valueArray();
-
-    REQUIRE(value_array.size() == 9);
-
-    REQUIRE(value_array[0] == 0);
-    REQUIRE(value_array[1] == 5);
-    REQUIRE(value_array[2] == 1);
-    REQUIRE(value_array[3] == 11);
-    REQUIRE(value_array[4] == 4);
-    REQUIRE(value_array[5] == -3);
-    REQUIRE(value_array[6] == 5);
-    REQUIRE(value_array[7] == 1);
-    REQUIRE(value_array[8] == -2);
-  }
-
-  SECTION("output")
-  {
-    SparseMatrixDescriptor<int, uint8_t> S{5};
-    S(0, 2) = 3;
-    S(1, 2) = 11;
-    S(1, 1) = 1;
-    S(0, 2) += 2;
-    S(3, 3) = 5;
-    S(3, 1) = -3;
-    S(4, 1) = 1;
-    S(2, 2) = 4;
-    S(4, 4) = -2;
-
-    std::ostringstream output;
-    output << '\n' << S;
-
-    std::string expected_output = R"(
-0 | 0:0 2:5
-1 | 1:1 2:11
-2 | 2:4
-3 | 1:-3 3:5
-4 | 1:1 4:-2
-)";
-    REQUIRE(output.str() == expected_output);
-  }
-}
-- 
GitLab