Working with sparse linear solvers #785
-
Hi all, I am building a Python framework for numerical geodynamic modelling that uses all the tricks to get high performance with simple code. One of the features that I am working on is writing a convenient python interface for a wide range of direct linear system solvers. This is extremely important as we often deal with extremely ill posed problems and it is helpful to test out different solvers quickly. All solvers I want to expose follow the Eigen sparse solver concept and have identical methods, so let's take PardisoLU for example. Solving a sparse linear system involves 3 key steps:
The models we work with require the solution of a sparse linear system at each timestep. The values of the matrix change at each iteration, but the sparsity structure is often identical over the whole simulation. This means that it is possible to perform step 1) only once and then store and reuse the computed permutation in order to gain a substantial speedup. I am now looking for the best approach to implement bindings for my Eigen solver objects. The first attempt I made was to wrap the solver object in a shared_ptr for shared ownership as well as using nb::DRef to automatically map my scipy CSC matrix to Eigen::SparseMatrix objects: #include <nanobind/nanobind.h>
#include <nanobind/stl/shared_ptr.h>
#include <nanobind/eigen/dense.h>
#include <iostream>
// Eigen modules
#include <Eigen/Sparse>
#include <Eigen/PardisoSupport>
namespace nb = nanobind;
using SparseMatrix = Eigen::SparseMatrix<double, Eigen::ColMajor>;
using SparseSolver = Eigen::PardisoLU<SparseMatrix>;
using SolverPtr = std::shared_ptr<SparseSolver>;
// Function to create a solver object, returning it as a void* for Python
SolverPtr create_solver() {
return std::make_shared<SparseSolver>();
}
// Function to analyze the pattern of the matrix
void analyze_pattern(SolverPtr solver, nb::DRef<SparseMatrix> matrix) {
solver->analyzePattern(matrix);
if (solver->info() != Eigen::Success) {
throw std::runtime_error("Symbolic factorization failed");
}
}
// Function to factorize the matrix
void factorize(SolverPtr solver, nb::DRef<SparseMatrix> matrix){
solver->factorize(matrix);
if (solver->info() != Eigen::Success) {
throw std::runtime_error("Numerical factorization failed");
}
}
// Function to solve the system
Eigen::VectorXd solve(SolverPtr solver, const nb::DRef<Eigen::VectorXd> & b) {
Eigen::VectorXd x = solver->solve(b);
if (solver->info() != Eigen::Success) {
throw std::runtime_error("Solution failed");
}
return x;
}
// Wrapper functions for Python
NB_MODULE(pardiso_support, m) {
m.def("create_solver", &create_solver,
"Create a PardisoLU solver object");
m.def("analyze_pattern", &analyze_pattern,
"Perform symbolic factorization of the matrix",
nb::arg("solver"), nb::arg("matrix"));
m.def("factorize", &factorize,
"Perform numerical factorization of the matrix",
nb::arg("solver"), nb::arg("matrix"));
m.def("solve", &solve,
"Solve the linear system",
nb::arg("solver"), nb::arg("b"));
} This however has a few issue:
To get around this, I have tried to:
However this not only feels like a hack, but also non-deterministically results in:
What is the best way to go about this? |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 3 replies
-
To my knowledge, |
Beta Was this translation helpful? Give feedback.
Nevermind what I said.
I must've messed up something because using nb::capsule does indeed work!
Except having to explicitly define
noexcept
when initialising the capsule, this approach is easy and avoids having to re-initialize objects many times. Also object ownership is very clear so this is a plus!