From 67e7be27ec7139732a6bd33a783dc3f280e05cdf Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Tue, 1 Jul 2025 12:11:23 +0200 Subject: [PATCH 01/43] decompositions: Expose classes --- CMakeLists.txt | 5 + include/eigenpy/decompositions/FullPivLU.hpp | 194 ++++++++++++++++++ .../eigenpy/decompositions/PartialPivLU.hpp | 119 +++++++++++ include/eigenpy/decompositions/sparse/LU.hpp | 122 +++++++++++ include/eigenpy/decompositions/sparse/QR.hpp | 102 +++++++++ src/decompositions/decompositions.cpp | 8 + src/decompositions/fullpivlu-solver.cpp | 12 ++ src/decompositions/partialpivlu-solver.cpp | 12 ++ src/decompositions/sparse-lu-solver.cpp | 13 ++ src/decompositions/sparse-qr-solver.cpp | 13 ++ unittest/CMakeLists.txt | 10 + .../decompositions/sparse/test_SparseLU.py | 48 +++++ .../decompositions/sparse/test_SparseQR.py | 46 +++++ unittest/python/test_FullPivLU.py | 46 +++++ unittest/python/test_PartialPivLU.py | 29 +++ 15 files changed, 779 insertions(+) create mode 100644 include/eigenpy/decompositions/FullPivLU.hpp create mode 100644 include/eigenpy/decompositions/PartialPivLU.hpp create mode 100644 include/eigenpy/decompositions/sparse/LU.hpp create mode 100644 include/eigenpy/decompositions/sparse/QR.hpp create mode 100644 src/decompositions/fullpivlu-solver.cpp create mode 100644 src/decompositions/partialpivlu-solver.cpp create mode 100644 src/decompositions/sparse-lu-solver.cpp create mode 100644 src/decompositions/sparse-qr-solver.cpp create mode 100644 unittest/python/decompositions/sparse/test_SparseLU.py create mode 100644 unittest/python/decompositions/sparse/test_SparseQR.py create mode 100644 unittest/python/test_FullPivLU.py create mode 100644 unittest/python/test_PartialPivLU.py diff --git a/CMakeLists.txt b/CMakeLists.txt index e82a3c6ae..1f84821b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -210,6 +210,7 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_ACCELERATE_HEADERS set(${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_HEADERS include/eigenpy/decompositions/sparse/LLT.hpp include/eigenpy/decompositions/sparse/LDLT.hpp + include/eigenpy/decompositions/sparse/LU.hpp include/eigenpy/decompositions/sparse/SimplicialCholesky.hpp include/eigenpy/decompositions/sparse/SparseSolverBase.hpp) @@ -308,7 +309,11 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_SOURCES src/decompositions/eigen-solver.cpp src/decompositions/llt-solver.cpp src/decompositions/ldlt-solver.cpp + src/decompositions/fullpivlu-solver.cpp + src/decompositions/partialpivlu-solver.cpp src/decompositions/minres-solver.cpp + src/decompositions/sparse-lu-solver.cpp + src/decompositions/sparse-qr-solver.cpp src/decompositions/qr-solvers.cpp src/decompositions/eigen-solver.cpp src/decompositions/self-adjoint-eigen-solver.cpp diff --git a/include/eigenpy/decompositions/FullPivLU.hpp b/include/eigenpy/decompositions/FullPivLU.hpp new file mode 100644 index 000000000..e95666db5 --- /dev/null +++ b/include/eigenpy/decompositions/FullPivLU.hpp @@ -0,0 +1,194 @@ +/* + * Copyright 2020-2025 INRIA + */ + +#ifndef __eigenpy_decompositions_fullpivlu_hpp__ +#define __eigenpy_decompositions_fullpivlu_hpp__ + +#include +#include + +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" +#include "eigenpy/eigen/EigenBase.hpp" + +namespace eigenpy { + +template +struct FullPivLUSolverVisitor + : public boost::python::def_visitor> { + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef typename MatrixType::RealScalar RealScalar; + typedef Eigen::Matrix + VectorXs; + typedef Eigen::Matrix + MatrixXs; + typedef Eigen::FullPivLU Solver; + + template + void visit(PyClass &cl) const { + cl.def(bp::init<>(bp::arg("self"), "Default constructor")) + .def(bp::init( + bp::args("self", "rows", "cols"), + "Default constructor with memory preallocation")) + .def(bp::init(bp::args("self", "matrix"), + "Constructor.")) + .def(bp::init( + bp::args("self", "matrix"), + "Constructs a LU factorization from a given matrix.")) + + .def(EigenBaseVisitor()) + + .def( + "compute", + (Solver & (Solver::*)(const Eigen::EigenBase &matrix)) & + Solver::compute, + bp::args("self", "matrix"), + "Computes the LU decomposition of the given matrix.", + bp::return_self<>()) + + .def("determinant", &Solver::determinant, bp::arg("self"), + "Returns the determinant of the matrix of which *this is the LU " + "decomposition.") + .def("dimensionOfKernel", &Solver::dimensionOfKernel, bp::arg("self"), + "Returns the dimension of the kernel of the matrix of which *this " + "is the LU decomposition.") + .def( + "image", + +[](const Solver &self, const MatrixType &mat) -> MatrixType { + return self.image(mat); + }, + bp::args("self", "originalMatrix"), + "Returns the image of the matrix, also called its column-space. " + "The columns of the returned matrix will form a basis of the " + "image (column-space).") + .def( + "inverse", + +[](const Solver &self) -> MatrixType { return self.inverse(); }, + bp::arg("self"), + "Returns the inverse of the matrix of which *this is the LU " + "decomposition.") + + .def("isInjective", &Solver::isInjective, bp::arg("self")) + .def("isInvertible", &Solver::isInvertible, bp::arg("self")) + .def("isSurjective", &Solver::isSurjective, bp::arg("self")) + + .def( + "kernel", + +[](const Solver &self) -> MatrixType { return self.kernel(); }, + bp::arg("self"), + "Returns the kernel of the matrix, also called its null-space. " + "The columns of the returned matrix will form a basis of the " + "kernel.") + + .def("matrixLU", &Solver::matrixLU, bp::arg("self"), + "Returns the LU decomposition matrix.", + bp::return_internal_reference<>()) + + .def("maxPivot", &Solver::maxPivot, bp::arg("self")) + .def("nonzeroPivots", &Solver::nonzeroPivots, bp::arg("self")) + + .def("permutationP", &Solver::permutationP, bp::arg("self"), + "Returns the permutation P.", + bp::return_value_policy()) + .def("permutationQ", &Solver::permutationQ, bp::arg("self"), + "Returns the permutation Q.", + bp::return_value_policy()) + + .def("rank", &Solver::rank, bp::arg("self")) + +#if EIGEN_VERSION_AT_LEAST(3, 3, 0) + .def("rcond", &Solver::rcond, bp::arg("self"), + "Returns an estimate of the reciprocal condition number of the " + "matrix.") +#endif + .def("reconstructedMatrix", &Solver::reconstructedMatrix, + bp::arg("self"), + "Returns the matrix represented by the decomposition, i.e., it " + "returns the product: P-1LUQ-1. This function is provided for " + "debug " + "purpose.") + + .def("setThreshold", + (Solver & (Solver::*)(const RealScalar &)) & Solver::setThreshold, + bp::args("self", "threshold"), + "Allows to prescribe a threshold to be used by certain methods, " + "such as rank(), who need to determine when pivots are to be " + "considered nonzero. This is not used for the LU decomposition " + "itself.\n" + "\n" + "When it needs to get the threshold value, Eigen calls " + "threshold(). By default, this uses a formula to automatically " + "determine a reasonable threshold. Once you have called the " + "present method setThreshold(const RealScalar&), your value is " + "used instead.\n" + "\n" + "Note: A pivot will be considered nonzero if its absolute value " + "is strictly greater than |pivot| ⩽ threshold×|maxpivot| where " + "maxpivot is the biggest pivot.", + bp::return_self<>()) + .def("setThreshold", + (Solver & (Solver::*)(Eigen::Default_t)) & Solver::setThreshold, + bp::args("self", "threshold"), + "Allows to come back to the default behavior, letting Eigen use " + " its default formula for determining the threshold.", + bp::return_self<>()) + + .def("solve", &solve, bp::args("self", "b"), + "Returns the solution x of A x = b using the current " + "decomposition of A.") + .def("solve", &solve, bp::args("self", "B"), + "Returns the solution X of A X = B using the current " + "decomposition of A where B is a right hand side matrix.") + + .def("threshold", &Solver::threshold, bp::arg("self"), + "Returns the threshold that will be used by certain methods such " + "as rank()."); + } + + static void expose() { + static const std::string classname = + "FullPivLU" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string &name) { + bp::class_( + name.c_str(), + "LU decomposition of a matrix with complete pivoting, and related " + "features.\n\n" + "This class represents a LU decomposition of any matrix, with complete " + "pivoting: the matrix A is decomposed as A=P−1LUQ−1 where L is " + "unit-lower-triangular, U is upper-triangular, and P and Q are " + "permutation matrices. This is a rank-revealing LU decomposition. " + "The eigenvalues (diagonal coefficients) of U are sorted in such a " + "way that any zeros are at the end.\n\n" + "This decomposition provides the generic approach to solving systems " + "of " + "linear equations, computing the rank, invertibility, inverse, kernel, " + "and determinant. \n\n" + "This LU decomposition is very stable and well tested with large " + "matrices. " + "However there are use cases where the SVD decomposition is inherently " + "more " + "stable and/or flexible. For example, when computing the kernel of a " + "matrix, " + "working with the SVD allows to select the smallest singular values of " + "the matrix, something that the LU decomposition doesn't see.", + bp::no_init) + .def(IdVisitor()) + .def(FullPivLUSolverVisitor()); + } + + private: + template + static MatrixOrVector solve(const Solver &self, const MatrixOrVector &vec) { + return self.solve(vec); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_fullpivlu_hpp__ diff --git a/include/eigenpy/decompositions/PartialPivLU.hpp b/include/eigenpy/decompositions/PartialPivLU.hpp new file mode 100644 index 000000000..de0f6a4ea --- /dev/null +++ b/include/eigenpy/decompositions/PartialPivLU.hpp @@ -0,0 +1,119 @@ +/* + * Copyright 2020-2025 INRIA + */ + +#ifndef __eigenpy_decompositions_partialpivlu_hpp__ +#define __eigenpy_decompositions_partialpivlu_hpp__ + +#include +#include + +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" +#include "eigenpy/eigen/EigenBase.hpp" + +namespace eigenpy { + +template +struct PartialPivLUSolverVisitor : public boost::python::def_visitor< + PartialPivLUSolverVisitor<_MatrixType>> { + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef typename MatrixType::RealScalar RealScalar; + typedef Eigen::Matrix + VectorXs; + typedef Eigen::Matrix + MatrixXs; + typedef Eigen::PartialPivLU Solver; + + template + void visit(PyClass &cl) const { + cl.def(bp::init<>(bp::arg("self"), "Default constructor")) + .def(bp::init( + bp::args("self", "size"), + "Default constructor with memory preallocation")) + .def(bp::init(bp::args("self", "matrix"), + "Constructor.")) + .def(bp::init( + bp::args("self", "matrix"), + "Constructs a LU factorization from a given matrix.")) + + .def(EigenBaseVisitor()) + + .def("determinant", &Solver::determinant, bp::arg("self"), + "Returns the determinant of the matrix of which *this is the LU " + "decomposition.") + .def( + "inverse", + +[](const Solver &self) -> MatrixType { return self.inverse(); }, + bp::arg("self"), + "Returns the inverse of the matrix of which *this is the LU " + "decomposition.") + .def("matrixLU", &Solver::matrixLU, bp::arg("self"), + "Returns the LU decomposition matrix.", + bp::return_internal_reference<>()) + .def("permutationP", &Solver::permutationP, bp::arg("self"), + "Returns the permutation P.", + bp::return_value_policy()) +#if EIGEN_VERSION_AT_LEAST(3, 3, 0) + .def("rcond", &Solver::rcond, bp::arg("self"), + "Returns an estimate of the reciprocal condition number of the " + "matrix.") +#endif + .def("reconstructedMatrix", &Solver::reconstructedMatrix, + bp::arg("self"), + "Returns the matrix represented by the decomposition, i.e., it " + "returns the product: P-1LUQ-1. This function is provided for " + "debug " + "purpose.") + .def("solve", &solve, bp::args("self", "b"), + "Returns the solution x of A x = b using the current " + "decomposition of A.") + .def("solve", &solve, bp::args("self", "B"), + "Returns the solution X of A X = B using the current " + "decomposition of A where B is a right hand side matrix."); + } + + static void expose() { + static const std::string classname = + "FullPivLU" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string &name) { + bp::class_(name.c_str(), + "LU decomposition of a matrix with partial pivoting, " + "and related features. \n\n" + "This class represents a LU decomposition of a square " + "invertible matrix, " + "with partial pivoting: the matrix A is decomposed as A " + "= PLU where L is " + "unit-lower-triangular, U is upper-triangular, and P is " + "a permutation matrix.\n\n" + "Typically, partial pivoting LU decomposition is only " + "considered numerically " + "stable for square invertible matrices. Thus LAPACK's " + "dgesv and dgesvx require " + "the matrix to be square and invertible. The present " + "class does the same. It " + "will assert that the matrix is square, but it won't " + "(actually it can't) check " + "that the matrix is invertible: it is your task to " + "check that you only use this " + "decomposition on invertible matrices.", + bp::no_init) + .def(IdVisitor()) + .def(PartialPivLUSolverVisitor()); + } + + private: + template + static MatrixOrVector solve(const Solver &self, const MatrixOrVector &vec) { + return self.solve(vec); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_partialpivlu_hpp__ diff --git a/include/eigenpy/decompositions/sparse/LU.hpp b/include/eigenpy/decompositions/sparse/LU.hpp new file mode 100644 index 000000000..c25e49586 --- /dev/null +++ b/include/eigenpy/decompositions/sparse/LU.hpp @@ -0,0 +1,122 @@ +/* + * Copyright 2024 INRIA + */ + +#ifndef __eigenpy_decompositions_sparse_lu_hpp__ +#define __eigenpy_decompositions_sparse_lu_hpp__ + +#include +#include + +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/decompositions/sparse/SparseSolverBase.hpp" +#include "eigenpy/utils/scalar-name.hpp" + +namespace eigenpy { + +template > +struct SparseLUVisitor : public boost::python::def_visitor< + SparseLUVisitor<_MatrixType, _Ordering>> { + typedef SparseLUVisitor<_MatrixType, _Ordering> Visitor; + typedef _MatrixType MatrixType; + + typedef Eigen::SparseLU Solver; + typedef typename MatrixType::Scalar Scalar; + typedef typename MatrixType::RealScalar RealScalar; + typedef Eigen::Matrix + DenseVectorXs; + typedef Eigen::Matrix + DenseMatrixXs; + + template + void visit(PyClass &cl) const { + cl.def(bp::init<>(bp::arg("self"), "Default constructor")) + .def(bp::init(bp::args("self", "matrix"), + "Constructs and performs the LU " + "factorization from a given matrix.")) + .def("absDeterminant", &Solver::absDeterminant, bp::arg("self"), + "Returns the absolute value of the determinant of the matrix of " + "which *this is the QR decomposition.") + .def("analyzePattern", &Solver::analyzePattern, bp::args("self", "mat"), + "Compute the column permutation to minimize the fill-in.") + .def("colsPermutation", &Solver::colsPermutation, bp::arg("self"), + "Returns a reference to the column matrix permutation PTc such " + "that Pr A PTc = LU.", + bp::return_value_policy()) + .def("compute", &Solver::compute, bp::args("self", "matrix"), + "Compute the symbolic and numeric factorization of the input " + "sparse matrix. " + "The input matrix should be in column-major storage. ") + .def("determinant", &Solver::determinant, bp::arg("self"), + "Returns the determinant of the matrix.") + .def("factorize", &Solver::factorize, bp::args("self", "matrix"), + "Performs a numeric decomposition of a given matrix.\n" + "The given matrix must has the same sparcity than the matrix on " + "which the symbolic decomposition has been performed.") + + .def("info", &Solver::info, bp::arg("self"), + "NumericalIssue if the input contains INF or NaN values or " + "overflow occured. Returns Success otherwise.") + + .def("isSymmetric", &Solver::isSymmetric, bp::args("self", "sym"), + "Indicate that the pattern of the input matrix is symmetric. ") + .def("lastErrorMessage", &Solver::lastErrorMessage, bp::arg("self"), + "Returns a string describing the type of error. ") + .def("logAbsDeterminant", &Solver::logAbsDeterminant, bp::arg("self"), + "Returns the natural log of the absolute value of the determinant " + "of the " + "matrix of which *this is the QR decomposition") + + .def("rowsPermutation", &Solver::rowsPermutation, bp::arg("self"), + "Returns a reference to the row matrix permutation Pr such that " + "Pr A PTc = LU", + bp::return_value_policy()) + .def("setPivotThreshold", &Solver::setPivotThreshold, + bp::args("self", "thresh"), + "Set the threshold used for a diagonal entry to be an acceptable " + "pivot.") + .def("signDeterminant", &Solver::signDeterminant, bp::arg("self"), + "A number representing the sign of the determinant. ") + + .def(SparseSolverBaseVisitor()); + } + + static void expose() { + static const std::string classname = + "SparseLU_" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string &name) { + bp::class_( + name.c_str(), + "Sparse supernodal LU factorization for general matrices. \n\n" + "This class implements the supernodal LU factorization for general " + "matrices. " + "It uses the main techniques from the sequential SuperLU package " + "(http://crd-legacy.lbl.gov/~xiaoye/SuperLU/). It handles " + "transparently real " + "and complex arithmetic with single and double precision, depending on " + "the scalar " + "type of your input matrix. The code has been optimized to provide " + "BLAS-3 operations " + "during supernode-panel updates. It benefits directly from the " + "built-in high-performant " + "Eigen BLAS routines. Moreover, when the size of a supernode is very " + "small, the BLAS " + "calls are avoided to enable a better optimization from the compiler. " + "For best performance, " + "you should compile it with NDEBUG flag to avoid the numerous bounds " + "checking on vectors.", + bp::no_init) + .def(SparseLUVisitor()) + .def(IdVisitor()); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_sparse_lu_hpp__ diff --git a/include/eigenpy/decompositions/sparse/QR.hpp b/include/eigenpy/decompositions/sparse/QR.hpp new file mode 100644 index 000000000..0404dafd6 --- /dev/null +++ b/include/eigenpy/decompositions/sparse/QR.hpp @@ -0,0 +1,102 @@ +/* + * Copyright 2024 INRIA + */ + +#ifndef __eigenpy_decompositions_sparse_qr_hpp__ +#define __eigenpy_decompositions_sparse_qr_hpp__ + +#include +#include + +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/decompositions/sparse/SparseSolverBase.hpp" +#include "eigenpy/utils/scalar-name.hpp" + +namespace eigenpy { + +template > // TODO: Check ordering +struct SparseQRVisitor : public boost::python::def_visitor< + SparseQRVisitor<_MatrixType, _Ordering>> { + typedef SparseQRVisitor<_MatrixType, _Ordering> Visitor; + typedef _MatrixType MatrixType; + typedef _Ordering Ordering; + + typedef Eigen::SparseQR Solver; + typedef typename MatrixType::Scalar Scalar; + typedef typename MatrixType::RealScalar RealScalar; + typedef Eigen::Matrix + DenseVectorXs; + typedef Eigen::Matrix + DenseMatrixXs; + + typedef Eigen::SparseQRMatrixQReturnType MatrixQType; + + template + void visit(PyClass &cl) const { + cl.def(bp::init<>(bp::arg("self"), "Default constructor")) + .def(bp::init(bp::args("self", "mat"), + "Construct a QR factorization of the matrix mat.")) + .def("analyzePattern", &Solver::analyzePattern, bp::args("self", "mat"), + "Compute the column permutation to minimize the fill-in.") + .def("cols", &Solver::cols, bp::arg("self"), + "Returns the number of columns of the represented matrix. ") + .def("colsPermutation", &Solver::colsPermutation, bp::arg("self"), + "Returns a reference to the column matrix permutation PTc such " + "that Pr A PTc = LU.", + bp::return_value_policy()) + .def("compute", &Solver::compute, bp::args("self", "matrix"), + "Compute the symbolic and numeric factorization of the input " + "sparse matrix. " + "The input matrix should be in column-major storage. ") + .def("factorize", &Solver::factorize, bp::args("self", "matrix"), + "Performs a numeric decomposition of a given matrix.\n" + "The given matrix must has the same sparcity than the matrix on " + "which the symbolic decomposition has been performed.") + .def("info", &Solver::info, bp::arg("self"), + "NumericalIssue if the input contains INF or NaN values or " + "overflow occured. Returns Success otherwise.") + .def("lastErrorMessage", &Solver::lastErrorMessage, bp::arg("self"), + "Returns a string describing the type of error. ") + .def("rank", &Solver::rank, bp::arg("self"), + "Returns the number of non linearly dependent columns as determined " + "by the pivoting threshold. ") + .def("rows", &Solver::rows, bp::arg("self"), + "Returns the number of rows of the represented matrix. ") + .def("setPivotThreshold", &Solver::setPivotThreshold, + bp::args("self", "thresh"), + "Set the threshold used for a diagonal entry to be an acceptable " + "pivot.") + + .def(SparseSolverBaseVisitor()); + } + + static void expose() { + static const std::string classname = + "SparseQR_" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string &name) { + bp::class_( + name.c_str(), + "Sparse left-looking QR factorization with numerical column pivoting. " + "This class implements a left-looking QR decomposition of sparse matrices " + "with numerical column pivoting. When a column has a norm less than a given " + "tolerance it is implicitly permuted to the end. The QR factorization thus " + "obtained is given by A*P = Q*R where R is upper triangular or trapezoidal. \n\n" + "P is the column permutation which is the product of the fill-reducing and the " + "numerical permutations. \n\n" + "Q is the orthogonal matrix represented as products of Householder reflectors. \n\n" + "R is the sparse triangular or trapezoidal matrix. The later occurs when A is rank-deficient. \n\n", + bp::no_init) + .def(SparseQRVisitor()) + .def(IdVisitor()); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_sparse_qr_hpp__ diff --git a/src/decompositions/decompositions.cpp b/src/decompositions/decompositions.cpp index 77ae22f29..6d0974ced 100644 --- a/src/decompositions/decompositions.cpp +++ b/src/decompositions/decompositions.cpp @@ -12,10 +12,14 @@ void exposeEigenSolver(); void exposeSelfAdjointEigenSolver(); void exposeLLTSolver(); void exposeLDLTSolver(); +void exposeFullPivLUSolver(); +void exposePartialPivLUSolver(); void exposeQRSolvers(); void exposeMINRESSolver(); void exposeSimplicialLLTSolver(); void exposeSimplicialLDLTSolver(); +void exposeSparseLUSolver(); +void exposeSparseQRSolver(); void exposePermutationMatrix(); void exposeDecompositions() { @@ -25,6 +29,8 @@ void exposeDecompositions() { exposeSelfAdjointEigenSolver(); exposeLLTSolver(); exposeLDLTSolver(); + exposeFullPivLUSolver(); + exposePartialPivLUSolver(); exposeQRSolvers(); exposeMINRESSolver(); @@ -44,6 +50,8 @@ void exposeDecompositions() { // Expose sparse decompositions exposeSimplicialLLTSolver(); exposeSimplicialLDLTSolver(); + exposeSparseLUSolver(); + exposeSparseQRSolver(); exposePermutationMatrix(); diff --git a/src/decompositions/fullpivlu-solver.cpp b/src/decompositions/fullpivlu-solver.cpp new file mode 100644 index 000000000..44fcaa07d --- /dev/null +++ b/src/decompositions/fullpivlu-solver.cpp @@ -0,0 +1,12 @@ +/* + * Copyright 2025 INRIA + */ + +#include "eigenpy/decompositions/FullPivLU.hpp" + +namespace eigenpy { +void exposeFullPivLUSolver() { + using namespace Eigen; + FullPivLUSolverVisitor::expose("FullPivLU"); +} +} // namespace eigenpy diff --git a/src/decompositions/partialpivlu-solver.cpp b/src/decompositions/partialpivlu-solver.cpp new file mode 100644 index 000000000..c6ee7f9be --- /dev/null +++ b/src/decompositions/partialpivlu-solver.cpp @@ -0,0 +1,12 @@ +/* + * Copyright 2025 INRIA + */ + +#include "eigenpy/decompositions/PartialPivLU.hpp" + +namespace eigenpy { +void exposePartialPivLUSolver() { + using namespace Eigen; + PartialPivLUSolverVisitor::expose("PartialPivLU"); +} +} // namespace eigenpy diff --git a/src/decompositions/sparse-lu-solver.cpp b/src/decompositions/sparse-lu-solver.cpp new file mode 100644 index 000000000..2c27a1e88 --- /dev/null +++ b/src/decompositions/sparse-lu-solver.cpp @@ -0,0 +1,13 @@ +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/sparse/LU.hpp" + +namespace eigenpy { +void exposeSparseLUSolver() { + using namespace Eigen; + typedef SparseMatrix ColMajorSparseMatrix; + SparseLUVisitor::expose("SparseLU"); +} +} // namespace eigenpy diff --git a/src/decompositions/sparse-qr-solver.cpp b/src/decompositions/sparse-qr-solver.cpp new file mode 100644 index 000000000..7e00d36f4 --- /dev/null +++ b/src/decompositions/sparse-qr-solver.cpp @@ -0,0 +1,13 @@ +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/sparse/QR.hpp" + +namespace eigenpy { +void exposeSparseQRSolver() { + using namespace Eigen; + typedef SparseMatrix ColMajorSparseMatrix; + SparseQRVisitor::expose("SparseQR"); +} +} // namespace eigenpy diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 98485fbed..141d1cafb 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -145,6 +145,12 @@ add_python_eigenpy_lib_unit_test("py-LLT" "unittest/python/test_LLT.py") add_python_eigenpy_lib_unit_test("py-LDLT" "unittest/python/test_LDLT.py") +add_python_eigenpy_lib_unit_test("py-FullPivLU" + "unittest/python/test_FullPivLU.py") + +add_python_eigenpy_lib_unit_test("py-PartialPivLU" + "unittest/python/test_PartialPivLU.py") + add_python_eigenpy_lib_unit_test("py-id" "unittest/python/test_id.py") add_python_eigenpy_lib_unit_test("py-QR" "unittest/python/test_QR.py") @@ -182,6 +188,10 @@ if(BUILD_TESTING_SCIPY) add_python_eigenpy_unit_test( "py-SimplicialLDLT" "unittest/python/decompositions/sparse/test_SimplicialLDLT.py") + add_python_eigenpy_unit_test( + "py-SparseLU" "unittest/python/decompositions/sparse/test_SparseLU.py") + add_python_eigenpy_unit_test( + "py-SparseQR" "unittest/python/decompositions/sparse/test_SparseQR.py") if(BUILD_WITH_CHOLMOD_SUPPORT) add_python_eigenpy_unit_test( diff --git a/unittest/python/decompositions/sparse/test_SparseLU.py b/unittest/python/decompositions/sparse/test_SparseLU.py new file mode 100644 index 000000000..81fd71e50 --- /dev/null +++ b/unittest/python/decompositions/sparse/test_SparseLU.py @@ -0,0 +1,48 @@ +import numpy as np +import scipy +from scipy.sparse import csc_matrix + +import eigenpy + +dim = 100 +rng = np.random.default_rng() + +A = rng.random((dim, dim)) +A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) +A = csc_matrix(A) + +sparselu = eigenpy.SparseLU(A) + +assert sparselu.info() == eigenpy.ComputationInfo.Success + +# Solve +X = rng.random((dim, 20)) +B = A.dot(X) +X_est = sparselu.solve(B) +assert eigenpy.is_approx(X, X_est) +assert eigenpy.is_approx(A.dot(X_est), B) + +X_sparse = scipy.sparse.random(dim, 10) +B_sparse = A.dot(X_sparse) +B_sparse = B_sparse.tocsc(True) + +if not B_sparse.has_sorted_indices: + B_sparse.sort_indices() + +X_est = sparselu.solve(B_sparse) +assert eigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) +assert eigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) + +# Others +det = sparselu.determinant() +sign_det = sparselu.signDeterminant() +abs_det = sparselu.absDeterminant() +log_abs_det = sparselu.logAbsDeterminant() + +sparselu.analyzePattern(A) +sparselu.factorize(A) + +cols_permutation = sparselu.colsPermutation() +rows_permutation = sparselu.rowsPermutation() + +sparselu.setPivotThreshold(1e-8) diff --git a/unittest/python/decompositions/sparse/test_SparseQR.py b/unittest/python/decompositions/sparse/test_SparseQR.py new file mode 100644 index 000000000..eda8ff33f --- /dev/null +++ b/unittest/python/decompositions/sparse/test_SparseQR.py @@ -0,0 +1,46 @@ +import numpy as np +import scipy +from scipy.sparse import csc_matrix + +import eigenpy + +dim = 100 +rng = np.random.default_rng() + +A = rng.random((dim, dim)) +A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) +A = csc_matrix(A) + +sparseqr = eigenpy.SparseQR(A) + +assert sparseqr.info() == eigenpy.ComputationInfo.Success + +# Solve +X = rng.random((dim, 20)) +B = A.dot(X) +X_est = sparseqr.solve(B) +assert eigenpy.is_approx(X, X_est) +assert eigenpy.is_approx(A.dot(X_est), B) + +X_sparse = scipy.sparse.random(dim, 10) +B_sparse = A.dot(X_sparse) +B_sparse = B_sparse.tocsc(True) + +if not B_sparse.has_sorted_indices: + B_sparse.sort_indices() + +X_est = sparseqr.solve(B_sparse) +assert eigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) +assert eigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) + +# Others +cols = sparseqr.cols() +rows = sparseqr.rows() + +sparseqr.analyzePattern(A) +sparseqr.factorize(A) + +cols_permutation = sparseqr.colsPermutation() +rank = sparseqr.rank() + +sparseqr.setPivotThreshold(1e-8) diff --git a/unittest/python/test_FullPivLU.py b/unittest/python/test_FullPivLU.py new file mode 100644 index 000000000..c9dc76c37 --- /dev/null +++ b/unittest/python/test_FullPivLU.py @@ -0,0 +1,46 @@ +import numpy as np + +import eigenpy + +dim = 100 +rng = np.random.default_rng() + +A = rng.random((dim, dim)) +A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) + +fullpivlu = eigenpy.FullPivLU(A) + +# Solve +X = rng.random((dim, 20)) +B = A.dot(X) +X_est = fullpivlu.solve(B) +assert eigenpy.is_approx(X, X_est) +assert eigenpy.is_approx(A.dot(X_est), B) + +# Others +cols = fullpivlu.cols() +rows = fullpivlu.rows() + +det = fullpivlu.determinant() +dim_kernel = fullpivlu.dimensionOfKernel() +rank = fullpivlu.rank() +rcond = fullpivlu.rcond() +max_pivot = fullpivlu.maxPivot() +nonzero_pivots = fullpivlu.nonzeroPivots() + +is_injective = fullpivlu.isInjective() +is_invertible = fullpivlu.isInvertible() +is_surjective = fullpivlu.isSurjective() + +LU = fullpivlu.matrixLU() +permutationP = fullpivlu.permutationP() +permutationQ = fullpivlu.permutationQ() + +reconstructed_matrix = fullpivlu.reconstructedMatrix() + +fullpivlu.setThreshold(1e-8) +threshold = fullpivlu.threshold() + +image = fullpivlu.image(A) +inverse = fullpivlu.inverse() +kernel = fullpivlu.kernel() diff --git a/unittest/python/test_PartialPivLU.py b/unittest/python/test_PartialPivLU.py new file mode 100644 index 000000000..1d0cbbeb0 --- /dev/null +++ b/unittest/python/test_PartialPivLU.py @@ -0,0 +1,29 @@ +import numpy as np + +import eigenpy + +dim = 100 +rng = np.random.default_rng() + +A = rng.random((dim, dim)) +A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) + +partialpivlu = eigenpy.PartialPivLU(A) + +# Solve +X = rng.random((dim, 20)) +B = A.dot(X) +X_est = partialpivlu.solve(B) +assert eigenpy.is_approx(X, X_est) +assert eigenpy.is_approx(A.dot(X_est), B) + +# Others +cols = partialpivlu.cols() +rows = partialpivlu.rows() + +det = partialpivlu.determinant() +rcond = partialpivlu.rcond() + +matrixLU = partialpivlu.matrixLU() +permutationP = partialpivlu.permutationP() +reconstructed_matrix = partialpivlu.reconstructedMatrix() From aac8d031f236b5cdbecd5c40c1a901a5e33e82d2 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Wed, 2 Jul 2025 14:26:29 +0200 Subject: [PATCH 02/43] readme: added lucas haubert --- README.md | 1 + include/eigenpy/decompositions/sparse/QR.hpp | 33 +++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f5a5bae17..2552f83fc 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ The following people have been involved in the development of **EigenPy**: - [Loïc Estève](https://github.com/lesteve) (Inria): Conda integration - [Wilson Jallet](https://manifoldfr.github.io/) (Inria): core developer - [Joris Vaillant](https://github.com/jorisv) (Inria): core developer and manager of the project +- [Lucas Haubert](https://www.linkedin.com/in/lucas-haubert-b668a421a/) (Inria): core developer If you have taken part in the development of **EigenPy**, feel free to add your name and contribution here. diff --git a/include/eigenpy/decompositions/sparse/QR.hpp b/include/eigenpy/decompositions/sparse/QR.hpp index 0404dafd6..7ea902ad0 100644 --- a/include/eigenpy/decompositions/sparse/QR.hpp +++ b/include/eigenpy/decompositions/sparse/QR.hpp @@ -15,8 +15,8 @@ namespace eigenpy { template > // TODO: Check ordering + typename _Ordering = Eigen::COLAMDOrdering< + typename _MatrixType::StorageIndex>> // TODO: Check ordering struct SparseQRVisitor : public boost::python::def_visitor< SparseQRVisitor<_MatrixType, _Ordering>> { typedef SparseQRVisitor<_MatrixType, _Ordering> Visitor; @@ -37,8 +37,9 @@ struct SparseQRVisitor : public boost::python::def_visitor< template void visit(PyClass &cl) const { cl.def(bp::init<>(bp::arg("self"), "Default constructor")) - .def(bp::init(bp::args("self", "mat"), - "Construct a QR factorization of the matrix mat.")) + .def(bp::init( + bp::args("self", "mat"), + "Construct a QR factorization of the matrix mat.")) .def("analyzePattern", &Solver::analyzePattern, bp::args("self", "mat"), "Compute the column permutation to minimize the fill-in.") .def("cols", &Solver::cols, bp::arg("self"), @@ -61,7 +62,8 @@ struct SparseQRVisitor : public boost::python::def_visitor< .def("lastErrorMessage", &Solver::lastErrorMessage, bp::arg("self"), "Returns a string describing the type of error. ") .def("rank", &Solver::rank, bp::arg("self"), - "Returns the number of non linearly dependent columns as determined " + "Returns the number of non linearly dependent columns as " + "determined " "by the pivoting threshold. ") .def("rows", &Solver::rows, bp::arg("self"), "Returns the number of rows of the represented matrix. ") @@ -83,14 +85,21 @@ struct SparseQRVisitor : public boost::python::def_visitor< bp::class_( name.c_str(), "Sparse left-looking QR factorization with numerical column pivoting. " - "This class implements a left-looking QR decomposition of sparse matrices " - "with numerical column pivoting. When a column has a norm less than a given " - "tolerance it is implicitly permuted to the end. The QR factorization thus " - "obtained is given by A*P = Q*R where R is upper triangular or trapezoidal. \n\n" - "P is the column permutation which is the product of the fill-reducing and the " + "This class implements a left-looking QR decomposition of sparse " + "matrices " + "with numerical column pivoting. When a column has a norm less than a " + "given " + "tolerance it is implicitly permuted to the end. The QR factorization " + "thus " + "obtained is given by A*P = Q*R where R is upper triangular or " + "trapezoidal. \n\n" + "P is the column permutation which is the product of the fill-reducing " + "and the " "numerical permutations. \n\n" - "Q is the orthogonal matrix represented as products of Householder reflectors. \n\n" - "R is the sparse triangular or trapezoidal matrix. The later occurs when A is rank-deficient. \n\n", + "Q is the orthogonal matrix represented as products of Householder " + "reflectors. \n\n" + "R is the sparse triangular or trapezoidal matrix. The later occurs " + "when A is rank-deficient. \n\n", bp::no_init) .def(SparseQRVisitor()) .def(IdVisitor()); From 15aaa0713f23ed66da372223fa1685a36bb8cd63 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 3 Jul 2025 15:28:41 +0200 Subject: [PATCH 03/43] decompositions: SVDBase, BDCSVD, JacobiSVD --- CMakeLists.txt | 7 +- include/eigenpy/decompositions/BDCSVD.hpp | 111 +++++++++++++++++++ include/eigenpy/decompositions/JacobiSVD.hpp | 102 +++++++++++++++++ include/eigenpy/decompositions/SVDBase.hpp | 105 ++++++++++++++++++ src/decompositions/bdcsvd-solver.cpp | 12 ++ src/decompositions/decompositions.cpp | 4 + src/decompositions/jacobisvd-solver.cpp | 12 ++ src/decompositions/seigen-solver.cpp | 13 --- unittest/CMakeLists.txt | 5 + unittest/python/test_BDCSVD.py | 35 ++++++ unittest/python/test_JacobiSVD.py | 35 ++++++ 11 files changed, 427 insertions(+), 14 deletions(-) create mode 100644 include/eigenpy/decompositions/BDCSVD.hpp create mode 100644 include/eigenpy/decompositions/JacobiSVD.hpp create mode 100644 include/eigenpy/decompositions/SVDBase.hpp create mode 100644 src/decompositions/bdcsvd-solver.cpp create mode 100644 src/decompositions/jacobisvd-solver.cpp delete mode 100644 src/decompositions/seigen-solver.cpp create mode 100644 unittest/python/test_BDCSVD.py create mode 100644 unittest/python/test_JacobiSVD.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f84821b3..2b3aacdd8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -237,7 +237,10 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_HEADERS include/eigenpy/decompositions/CompleteOrthogonalDecomposition.hpp include/eigenpy/decompositions/FullPivHouseholderQR.hpp include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp - include/eigenpy/decompositions/minres.hpp) + include/eigenpy/decompositions/minres.hpp + include/eigenpy/decompositions/SVDBase.hpp + include/eigenpy/decompositions/BDCSVD.hpp + include/eigenpy/decompositions/JacobiSVD.hpp) set(${PROJECT_NAME}_HEADERS ${${PROJECT_NAME}_UTILS_HEADERS} @@ -309,6 +312,8 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_SOURCES src/decompositions/eigen-solver.cpp src/decompositions/llt-solver.cpp src/decompositions/ldlt-solver.cpp + src/decompositions/bdcsvd-solver.cpp + src/decompositions/jacobisvd-solver.cpp src/decompositions/fullpivlu-solver.cpp src/decompositions/partialpivlu-solver.cpp src/decompositions/minres-solver.cpp diff --git a/include/eigenpy/decompositions/BDCSVD.hpp b/include/eigenpy/decompositions/BDCSVD.hpp new file mode 100644 index 000000000..d3ffb1ec5 --- /dev/null +++ b/include/eigenpy/decompositions/BDCSVD.hpp @@ -0,0 +1,111 @@ +/* + * Copyright 2020-2024 INRIA + */ + +#ifndef __eigenpy_decompositions_bdcsvd_hpp__ +#define __eigenpy_decompositions_bdcsvd_hpp__ + +#include +#include + +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" +#include "eigenpy/eigen/EigenBase.hpp" +#include "eigenpy/decompositions/SVDBase.hpp" + +namespace eigenpy { + +template +struct BDCSVDVisitor + : public boost::python::def_visitor> { + typedef _MatrixType MatrixType; + typedef Eigen::BDCSVD Solver; + typedef typename MatrixType::Scalar Scalar; + + template + void visit(PyClass &cl) const { + cl.def(bp::init<>(bp::arg("self"), "Default constructor")) + .def(bp::init( + bp::args("self", "rows", "cols"), + "Default Constructor with memory preallocation. ")) + .def(bp::init( + bp::args("self", "rows", "cols", "computationOptions "), + "Default Constructor with memory preallocation. \n\n" + "Like the default constructor but with preallocation of the " + "internal " + "data according to the specified problem size and the " + "computationOptions. ")) + .def(bp::init( + bp::args("self", "matrix"), + "Constructor performing the decomposition of given matrix. ")) + .def(bp::init( + bp::args("self", "matrix", "computationOptions "), + "Constructor performing the decomposition of given matrix. \n\n" + "One cannot request unitiaries using both the Options template " + "parameter " + "and the constructor. If possible, prefer using the Options " + "template parameter.")) + + .def("cols", &Solver::cols, bp::arg("self"), + "Returns the number of columns. ") + .def("compute", + (Solver & (Solver::*)(const MatrixType &matrix)) & Solver::compute, + bp::args("self", "matrix"), + "Method performing the decomposition of given matrix. Computes " + "Thin/Full " + "unitaries U/V if specified using the Options template parameter " + "or the class constructor. ", + bp::return_self<>()) + .def("compute", + (Solver & (Solver::*)(const MatrixType &matrix, + unsigned int computationOptions)) & + Solver::compute, + bp::args("self", "matrix"), + "Method performing the decomposition of given matrix, as " + "specified by the computationOptions parameter. ", + bp::return_self<>()) + .def("rows", &Solver::rows, bp::arg("self"), + "Returns the number of rows. . ") + + .def(SVDBaseVisitor()); + } + + static void expose() { + static const std::string classname = + "BDCSVD_" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string &name) { + bp::class_( + name.c_str(), + "Class Bidiagonal Divide and Conquer SVD.\n\n" + "This class first reduces the input matrix to bi-diagonal form using " + "class " + "UpperBidiagonalization, and then performs a divide-and-conquer " + "diagonalization. " + "Small blocks are diagonalized using class JacobiSVD. You can control " + "the " + "switching size with the setSwitchSize() method, default is 16. For " + "small matrice " + "(<16), it is thus preferable to directly use JacobiSVD. For larger " + "ones, BDCSVD " + "is highly recommended and can several order of magnitude faster.\n\n" + "Warming: this algorithm is unlikely to provide accurate result when " + "compiled with " + "unsafe math optimizations. For instance, this concerns Intel's " + "compiler (ICC), which " + "performs such optimization by default unless you compile with the " + "-fp-model precise " + "option. Likewise, the -ffast-math option of GCC or clang will " + "significantly degrade the " + "accuracy.", + bp::no_init) + .def(BDCSVDVisitor()) + .def(IdVisitor()); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_bdcsvd_hpp__ diff --git a/include/eigenpy/decompositions/JacobiSVD.hpp b/include/eigenpy/decompositions/JacobiSVD.hpp new file mode 100644 index 000000000..9fa39958a --- /dev/null +++ b/include/eigenpy/decompositions/JacobiSVD.hpp @@ -0,0 +1,102 @@ +/* + * Copyright 2020-2024 INRIA + */ + +#ifndef __eigenpy_decompositions_jacobisvd_hpp__ +#define __eigenpy_decompositions_jacobisvd_hpp__ + +#include +#include + +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" +#include "eigenpy/eigen/EigenBase.hpp" +#include "eigenpy/decompositions/SVDBase.hpp" + +namespace eigenpy { + +template +struct JacobiSVDVisitor + : public boost::python::def_visitor> { + + typedef _MatrixType MatrixType; + typedef Eigen::JacobiSVD Solver; + typedef typename MatrixType::Scalar Scalar; + + template + void visit(PyClass &cl) const { + cl.def(bp::init<>(bp::arg("self"), "Default constructor")) + .def(bp::init( + bp::args("self", "rows", "cols"), + "Default Constructor with memory preallocation. ")) + .def(bp::init( + bp::args("self", "rows", "cols", "computationOptions "), + "Default Constructor with memory preallocation. \n\n" + "Like the default constructor but with preallocation of the internal " + "data according to the specified problem size and the computationOptions. ")) + .def(bp::init( + bp::args("self", "matrix"), + "Constructor performing the decomposition of given matrix. ")) + .def(bp::init( + bp::args("self", "matrix", "computationOptions "), + "Constructor performing the decomposition of given matrix. \n\n" + "One cannot request unitiaries using both the Options template parameter " + "and the constructor. If possible, prefer using the Options template parameter.")) + + .def("cols", &Solver::cols, bp::arg("self"), + "Returns the number of columns. ") + .def( + "compute", + (Solver & (Solver::*)(const MatrixType &matrix)) & + Solver::compute, + bp::args("self", "matrix"), + "Method performing the decomposition of given matrix. Computes Thin/Full " + "unitaries U/V if specified using the Options template parameter or the class constructor. ", + bp::return_self<>()) + .def( + "compute", + (Solver & (Solver::*)(const MatrixType &matrix, unsigned int computationOptions)) & + Solver::compute, + bp::args("self", "matrix"), + "Method performing the decomposition of given matrix, as specified by the computationOptions parameter. ", + bp::return_self<>()) + .def("rows", &Solver::rows, bp::arg("self"), + "Returns the number of rows. . ") + + .def(SVDBaseVisitor()); + } + + static void expose() { + static const std::string classname = + "JacobiSVD_" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string &name) { + bp::class_( + name.c_str(), + "Two-sided Jacobi SVD decomposition of a rectangular matrix. \n\n" + "SVD decomposition consists in decomposing any n-by-p matrix A as a product " + "A=USV∗ where U is a n-by-n unitary, V is a p-by-p unitary, and S is a n-by-p r" + "eal positive matrix which is zero outside of its main diagonal; the diagonal " + "entries of S are known as the singular values of A and the columns of U and V " + "are known as the left and right singular vectors of A respectively. \n\n" + "Singular values are always sorted in decreasing order. \n\n " + "This JacobiSVD decomposition computes only the singular values by default. " + "If you want U or V, you need to ask for them explicitly. \n\n" + "You can ask for only thin U or V to be computed, meaning the following. " + "In case of a rectangular n-by-p matrix, letting m be the smaller value among " + "n and p, there are only m singular vectors; the remaining columns of U and V " + "do not correspond to actual singular vectors. Asking for thin U or V means asking " + "for only their m first columns to be formed. So U is then a n-by-m matrix, and V " + "is then a p-by-m matrix. Notice that thin U and V are all you need for (least squares) " + "solving.", + bp::no_init) + .def(JacobiSVDVisitor()) + .def(IdVisitor()); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_jacobisvd_hpp__ diff --git a/include/eigenpy/decompositions/SVDBase.hpp b/include/eigenpy/decompositions/SVDBase.hpp new file mode 100644 index 000000000..8c20e87b8 --- /dev/null +++ b/include/eigenpy/decompositions/SVDBase.hpp @@ -0,0 +1,105 @@ +/* + * Copyright 2020-2024 INRIA + */ + +#ifndef __eigenpy_decompositions_svdbase_hpp__ +#define __eigenpy_decompositions_svdbase_hpp__ + +#include +#include + +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" +#include "eigenpy/eigen/EigenBase.hpp" + +namespace eigenpy { + +template +struct SVDBaseVisitor + : public boost::python::def_visitor> { + typedef Derived Solver; + + typedef typename Derived::MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef typename MatrixType::RealScalar RealScalar; + + typedef Eigen::Matrix + VectorXs; + typedef Eigen::Matrix + MatrixXs; + + template + void visit(PyClass &cl) const { + cl.def(bp::init<>(bp::arg("self"), "Default constructor")) + + .def("computeU", &Solver::computeU, bp::arg("self"), + "Returns true if U (full or thin) is asked for in this " + "SVD decomposition ") + .def("computeV", &Solver::computeV, bp::arg("self"), + "Returns true if V (full or thin) is asked for in this " + "SVD decomposition ") + + .def("info", &Solver::info, bp::arg("self"), + "Reports whether previous computation was successful. ") + + .def("matrixU", &matrixU, bp::arg("self"), "Returns the matrix U.") + .def("matrixV", &matrixV, bp::arg("self"), "Returns the matrix V.") + + .def("nonzeroSingularValues", &Solver::nonzeroSingularValues, + bp::arg("self"), + "Returns the number of singular values that are not exactly 0 ") + .def("rank", &Solver::rank, bp::arg("self"), + "the rank of the matrix of which *this is the SVD. ") + + .def("setThreshold", + (Solver & (Solver::*)(const RealScalar &)) & Solver::setThreshold, + bp::args("self", "threshold"), + "Allows to prescribe a threshold to be used by certain methods, " + "such as " + "rank() and solve(), which need to determine when singular values " + "are " + "to be considered nonzero. This is not used for the SVD " + "decomposition " + "itself.\n" + "\n" + "When it needs to get the threshold value, Eigen calls " + "threshold(). " + "The default is NumTraits::epsilon()", + bp::return_self<>()) + .def("setThreshold", + (Solver & (Solver::*)(Eigen::Default_t)) & Solver::setThreshold, + bp::args("self", "threshold"), + "Allows to come back to the default behavior, letting Eigen use " + " its default formula for determining the threshold.", + bp::return_self<>()) + + .def("singularValues", &Solver::singularValues, bp::arg("self"), + "Returns the vector of singular values.", + bp::return_value_policy()) + + .def("solve", &solve, bp::args("self", "b"), + "Returns the solution x of A x = b using the current " + "decomposition of A.") + .def("solve", &solve, bp::args("self", "B"), + "Returns the solution X of A X = B using the current " + "decomposition of A where B is a right hand side matrix.") + + .def("threshold", &Solver::threshold, bp::arg("self"), + "Returns the threshold that will be used by certain methods such " + "as rank(). "); + } + + private: + static MatrixXs matrixU(const Solver &self) { return self.matrixU(); } + static MatrixXs matrixV(const Solver &self) { return self.matrixV(); } + + template + static MatrixOrVector solve(const Solver &self, const MatrixOrVector &vec) { + return self.solve(vec); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_svdbase_hpp__ diff --git a/src/decompositions/bdcsvd-solver.cpp b/src/decompositions/bdcsvd-solver.cpp new file mode 100644 index 000000000..c691a426f --- /dev/null +++ b/src/decompositions/bdcsvd-solver.cpp @@ -0,0 +1,12 @@ +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/BDCSVD.hpp" + +namespace eigenpy { +void exposeBDCSVDSolver() { + using namespace Eigen; + BDCSVDVisitor::expose("BDCSVD"); +} +} // namespace eigenpy diff --git a/src/decompositions/decompositions.cpp b/src/decompositions/decompositions.cpp index 6d0974ced..5f0933a09 100644 --- a/src/decompositions/decompositions.cpp +++ b/src/decompositions/decompositions.cpp @@ -21,6 +21,8 @@ void exposeSimplicialLDLTSolver(); void exposeSparseLUSolver(); void exposeSparseQRSolver(); void exposePermutationMatrix(); +void exposeBDCSVDSolver(); +void exposeJacobiSVDSolver(); void exposeDecompositions() { using namespace Eigen; @@ -33,6 +35,8 @@ void exposeDecompositions() { exposePartialPivLUSolver(); exposeQRSolvers(); exposeMINRESSolver(); + exposeBDCSVDSolver(); + exposeJacobiSVDSolver(); { bp::enum_("DecompositionOptions") diff --git a/src/decompositions/jacobisvd-solver.cpp b/src/decompositions/jacobisvd-solver.cpp new file mode 100644 index 000000000..5b2234861 --- /dev/null +++ b/src/decompositions/jacobisvd-solver.cpp @@ -0,0 +1,12 @@ +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/JacobiSVD.hpp" + +namespace eigenpy { +void exposeJacobiSVDSolver() { + using namespace Eigen; + JacobiSVDVisitor::expose("JacobiSVD"); +} +} // namespace eigenpy diff --git a/src/decompositions/seigen-solver.cpp b/src/decompositions/seigen-solver.cpp deleted file mode 100644 index 8fdfbf4af..000000000 --- a/src/decompositions/seigen-solver.cpp +++ /dev/null @@ -1,13 +0,0 @@ - -/* - * Copyright 2024 INRIA - */ - -#include "eigenpy/decompositions/EigenSolver.hpp" - -namespace eigenpy { -void exposeEigenSolver() { - using namespace Eigen; - EigenSolverVisitor::expose("EigenSolver"); -} -} // namespace eigenpy diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 141d1cafb..0df3622df 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -155,6 +155,11 @@ add_python_eigenpy_lib_unit_test("py-id" "unittest/python/test_id.py") add_python_eigenpy_lib_unit_test("py-QR" "unittest/python/test_QR.py") +add_python_eigenpy_lib_unit_test("py-BDCSVD" "unittest/python/test_BDCSVD.py") + +add_python_eigenpy_lib_unit_test("py-JacobiSVD" + "unittest/python/test_JacobiSVD.py") + if(NOT WIN32) add_python_eigenpy_lib_unit_test("py-MINRES" "unittest/python/test_MINRES.py") endif(NOT WIN32) diff --git a/unittest/python/test_BDCSVD.py b/unittest/python/test_BDCSVD.py new file mode 100644 index 000000000..472f23a95 --- /dev/null +++ b/unittest/python/test_BDCSVD.py @@ -0,0 +1,35 @@ +import numpy as np + +import eigenpy + +dim = 100 +rng = np.random.default_rng() + +A = rng.random((dim, dim)) +A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) + +bdcsvd = eigenpy.BDCSVD(A, 24) + +# Solve +X = rng.random((dim, 20)) +B = A.dot(X) +X_est = bdcsvd.solve(B) +assert eigenpy.is_approx(X, X_est) +assert eigenpy.is_approx(A.dot(X_est), B) + +# Others +cols = bdcsvd.cols() +rows = bdcsvd.rows() + +comp_U = bdcsvd.computeU() +comp_V = bdcsvd.computeV() + +U = bdcsvd.matrixU() +V = bdcsvd.matrixV() + +nonzerosingval = bdcsvd.nonzeroSingularValues() +singularvalues = bdcsvd.singularValues() + +bdcsvd.setThreshold(1e-8) +threshold = bdcsvd.threshold() +rank = bdcsvd.rank() diff --git a/unittest/python/test_JacobiSVD.py b/unittest/python/test_JacobiSVD.py new file mode 100644 index 000000000..707ba6e81 --- /dev/null +++ b/unittest/python/test_JacobiSVD.py @@ -0,0 +1,35 @@ +import numpy as np + +import eigenpy + +dim = 100 +rng = np.random.default_rng() + +A = rng.random((dim, dim)) +A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) + +jacobisvd = eigenpy.JacobiSVD(A, 24) + +# Solve +X = rng.random((dim, 20)) +B = A.dot(X) +X_est = jacobisvd.solve(B) +assert eigenpy.is_approx(X, X_est) +assert eigenpy.is_approx(A.dot(X_est), B) + +# Others +cols = jacobisvd.cols() +rows = jacobisvd.rows() + +comp_U = jacobisvd.computeU() +comp_V = jacobisvd.computeV() + +U = jacobisvd.matrixU() +V = jacobisvd.matrixV() + +nonzerosingval = jacobisvd.nonzeroSingularValues() +singularvalues = jacobisvd.singularValues() + +jacobisvd.setThreshold(1e-8) +threshold = jacobisvd.threshold() +rank = jacobisvd.rank() From bd1c0f580761d4205bbeaf9ef9bc10815136e003 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 3 Jul 2025 16:26:26 +0200 Subject: [PATCH 04/43] eigenvalues: Expose classes --- CMakeLists.txt | 17 +++- .../decompositions/ComplexEigenSolver.hpp | 86 ++++++++++++++++++ .../eigenpy/decompositions/ComplexSchur.hpp | 90 +++++++++++++++++++ .../decompositions/GeneralizedEigenSolver.hpp | 88 ++++++++++++++++++ .../GeneralizedSelfAdjointEigenSolver.hpp | 75 ++++++++++++++++ .../HessenbergDecomposition.hpp | 82 +++++++++++++++++ include/eigenpy/decompositions/JacobiSVD.hpp | 80 ++++++++++------- include/eigenpy/decompositions/RealQZ.hpp | 90 +++++++++++++++++++ include/eigenpy/decompositions/RealSchur.hpp | 90 +++++++++++++++++++ .../decompositions/Tridiagonalization.hpp | 83 +++++++++++++++++ src/decompositions/complex-eigen-solver.cpp | 13 +++ src/decompositions/complex-schur.cpp | 13 +++ src/decompositions/decompositions.cpp | 16 ++++ .../generalized-eigen-solver.cpp | 13 +++ .../generalized-self-adjoint-eigen-solver.cpp | 14 +++ .../hessenberg-decomposition.cpp | 13 +++ src/decompositions/real-qz.cpp | 13 +++ src/decompositions/real-schur.cpp | 13 +++ src/decompositions/tridiagonalization.cpp | 13 +++ unittest/CMakeLists.txt | 26 ++++++ unittest/python/test_complex_eigen_solver.py | 15 ++++ unittest/python/test_complex_schur.py | 16 ++++ .../python/test_generalized_eigen_solver.py | 26 ++++++ ...t_generalized_self_adjoint_eigen_solver.py | 26 ++++++ .../python/test_hessenberg_decomposition.py | 9 ++ unittest/python/test_real_qz.py | 18 ++++ unittest/python/test_real_schur.py | 14 +++ unittest/python/test_tridiagonalization.py | 20 +++++ 28 files changed, 1039 insertions(+), 33 deletions(-) create mode 100644 include/eigenpy/decompositions/ComplexEigenSolver.hpp create mode 100644 include/eigenpy/decompositions/ComplexSchur.hpp create mode 100644 include/eigenpy/decompositions/GeneralizedEigenSolver.hpp create mode 100644 include/eigenpy/decompositions/GeneralizedSelfAdjointEigenSolver.hpp create mode 100644 include/eigenpy/decompositions/HessenbergDecomposition.hpp create mode 100644 include/eigenpy/decompositions/RealQZ.hpp create mode 100644 include/eigenpy/decompositions/RealSchur.hpp create mode 100644 include/eigenpy/decompositions/Tridiagonalization.hpp create mode 100644 src/decompositions/complex-eigen-solver.cpp create mode 100644 src/decompositions/complex-schur.cpp create mode 100644 src/decompositions/generalized-eigen-solver.cpp create mode 100644 src/decompositions/generalized-self-adjoint-eigen-solver.cpp create mode 100644 src/decompositions/hessenberg-decomposition.cpp create mode 100644 src/decompositions/real-qz.cpp create mode 100644 src/decompositions/real-schur.cpp create mode 100644 src/decompositions/tridiagonalization.cpp create mode 100644 unittest/python/test_complex_eigen_solver.py create mode 100644 unittest/python/test_complex_schur.py create mode 100644 unittest/python/test_generalized_eigen_solver.py create mode 100644 unittest/python/test_generalized_self_adjoint_eigen_solver.py create mode 100644 unittest/python/test_hessenberg_decomposition.py create mode 100644 unittest/python/test_real_qz.py create mode 100644 unittest/python/test_real_schur.py create mode 100644 unittest/python/test_tridiagonalization.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b3aacdd8..228dd128f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -228,6 +228,14 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_HEADERS ${${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_HEADERS} include/eigenpy/decompositions/decompositions.hpp include/eigenpy/decompositions/EigenSolver.hpp + include/eigenpy/decompositions/GeneralizedEigenSolver.hpp + include/eigenpy/decompositions/GeneralizedSelfAdjointEigenSolver.hpp + include/eigenpy/decompositions/HessenbergDecomposition.hpp + include/eigenpy/decompositions/RealQZ.hpp + include/eigenpy/decompositions/Tridiagonalization.hpp + include/eigenpy/decompositions/RealSchur.hpp + include/eigenpy/decompositions/ComplexEigenSolver.hpp + include/eigenpy/decompositions/ComplexSchur.hpp include/eigenpy/decompositions/PermutationMatrix.hpp include/eigenpy/decompositions/LDLT.hpp include/eigenpy/decompositions/LLT.hpp @@ -310,17 +318,24 @@ set(${PROJECT_NAME}_SOLVERS_SOURCES src/solvers/preconditioners.cpp set(${PROJECT_NAME}_DECOMPOSITIONS_SOURCES src/decompositions/decompositions.cpp src/decompositions/eigen-solver.cpp + src/decompositions/generalized-eigen-solver.cpp + src/decompositions/generalized-self-adjoint-eigen-solver.cpp + src/decompositions/complex-eigen-solver.cpp + src/decompositions/complex-schur.cpp src/decompositions/llt-solver.cpp src/decompositions/ldlt-solver.cpp src/decompositions/bdcsvd-solver.cpp src/decompositions/jacobisvd-solver.cpp src/decompositions/fullpivlu-solver.cpp + src/decompositions/hessenberg-decomposition.cpp + src/decompositions/real-qz.cpp + src/decompositions/tridiagonalization.cpp + src/decompositions/real-schur.cpp src/decompositions/partialpivlu-solver.cpp src/decompositions/minres-solver.cpp src/decompositions/sparse-lu-solver.cpp src/decompositions/sparse-qr-solver.cpp src/decompositions/qr-solvers.cpp - src/decompositions/eigen-solver.cpp src/decompositions/self-adjoint-eigen-solver.cpp src/decompositions/permutation-matrix.cpp src/decompositions/simplicial-llt-solver.cpp diff --git a/include/eigenpy/decompositions/ComplexEigenSolver.hpp b/include/eigenpy/decompositions/ComplexEigenSolver.hpp new file mode 100644 index 000000000..8ab07a513 --- /dev/null +++ b/include/eigenpy/decompositions/ComplexEigenSolver.hpp @@ -0,0 +1,86 @@ +/* + * Copyright 2020 INRIA + */ + +#ifndef __eigenpy_decompositions_complex_eigen_solver_hpp__ +#define __eigenpy_decompositions_complex_eigen_solver_hpp__ + +#include +#include + +#include "eigenpy/eigen-to-python.hpp" +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" + +namespace eigenpy { + +template +struct ComplexEigenSolverVisitor : public boost::python::def_visitor< + ComplexEigenSolverVisitor<_MatrixType>> { + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef Eigen::ComplexEigenSolver Solver; + + template + void visit(PyClass& cl) const { + cl.def(bp::init<>("Default constructor")) + .def(bp::init( + bp::arg("size"), "Default constructor with memory preallocation")) + .def(bp::init>( + bp::args("matrix", "compute_eigen_vectors"), + "Computes eigendecomposition of given matrix")) + + .def("eigenvalues", &Solver::eigenvalues, bp::arg("self"), + "Returns the eigenvalues of given matrix.", + bp::return_internal_reference<>()) + .def("eigenvectors", &Solver::eigenvectors, bp::arg("self"), + "Returns the eigenvectors of given matrix.", + bp::return_internal_reference<>()) + + .def("compute", &ComplexEigenSolverVisitor::compute_proxy, + bp::args("self", "matrix"), + "Computes the eigendecomposition of given matrix.", + bp::return_self<>()) + .def("compute", + (Solver & + (Solver::*)(const Eigen::EigenBase& matrix, bool)) & + Solver::compute, + bp::args("self", "matrix", "compute_eigen_vectors"), + "Computes the eigendecomposition of given matrix.", + bp::return_self<>()) + + .def("info", &Solver::info, bp::arg("self"), + "NumericalIssue if the input contains INF or NaN values or " + "overflow occured. Returns Success otherwise.") + + .def("getMaxIterations", &Solver::getMaxIterations, bp::arg("self"), + "Returns the maximum number of iterations.") + .def("setMaxIterations", &Solver::setMaxIterations, + bp::args("self", "max_iter"), + "Sets the maximum number of iterations allowed.", + bp::return_self<>()); + } + + static void expose() { + static const std::string classname = + "ComplexEigenSolver" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string& name) { + bp::class_(name.c_str(), bp::no_init) + .def(ComplexEigenSolverVisitor()) + .def(IdVisitor()); + } + + private: + template + static Solver& compute_proxy(Solver& self, + const Eigen::EigenBase& matrix) { + return self.compute(matrix); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_complex_eigen_solver_hpp__ diff --git a/include/eigenpy/decompositions/ComplexSchur.hpp b/include/eigenpy/decompositions/ComplexSchur.hpp new file mode 100644 index 000000000..fb5586515 --- /dev/null +++ b/include/eigenpy/decompositions/ComplexSchur.hpp @@ -0,0 +1,90 @@ +/* + * Copyright 2020 INRIA + */ + +#ifndef __eigenpy_decompositions_complex_schur_hpp__ +#define __eigenpy_decompositions_complex_schur_hpp__ + +#include +#include + +#include "eigenpy/eigen-to-python.hpp" +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" + +namespace eigenpy { + +template +struct ComplexSchurVisitor + : public boost::python::def_visitor> { + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef Eigen::ComplexSchur Solver; + + template + void visit(PyClass& cl) const { + cl.def(bp::init(bp::arg("size"), "Default constructor")) + .def(bp::init>( + bp::args("matrix", "computeU"), "Computes Schur of given matrix")) + + .def("compute", &ComplexSchurVisitor::compute_proxy, + bp::args("self", "matrix"), "Computes the Schur of given matrix.", + bp::return_self<>()) + .def("compute", + (Solver & + (Solver::*)(const Eigen::EigenBase& matrix, bool)) & + Solver::compute, + bp::args("self", "matrix", "computeU"), + "Computes the Schur of given matrix.", bp::return_self<>()) + + .def("computeFromHessenberg", + (Solver & (Solver::*)(const Eigen::EigenBase& matrixH, + const Eigen::EigenBase& matrixQ, + bool)) & + Solver::computeFromHessenberg, + bp::args("self", "matrixH", "matrixQ", "computeU"), + "Compute Schur decomposition from a given Hessenberg matrix. ", + bp::return_self<>()) + + .def("matrixT", &Solver::matrixT, bp::arg("self"), + "Returns the triangular matrix in the Schur decomposition. ", + bp::return_value_policy()) + .def("matrixU", &Solver::matrixU, bp::arg("self"), + "Returns the unitary matrix in the Schur decomposition. ", + bp::return_value_policy()) + + .def("info", &Solver::info, bp::arg("self"), + "NumericalIssue if the input contains INF or NaN values or " + "overflow occured. Returns Success otherwise.") + + .def("getMaxIterations", &Solver::getMaxIterations, bp::arg("self"), + "Returns the maximum number of iterations.") + .def("setMaxIterations", &Solver::setMaxIterations, + bp::args("self", "max_iter"), + "Sets the maximum number of iterations allowed.", + bp::return_self<>()); + } + + static void expose() { + static const std::string classname = + "ComplexSchur" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string& name) { + bp::class_(name.c_str(), bp::no_init) + .def(ComplexSchurVisitor()) + .def(IdVisitor()); + } + + private: + template + static Solver& compute_proxy(Solver& self, + const Eigen::EigenBase& matrix) { + return self.compute(matrix); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_complex_schur_hpp__ diff --git a/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp b/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp new file mode 100644 index 000000000..6c14bfc14 --- /dev/null +++ b/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp @@ -0,0 +1,88 @@ +/* + * Copyright 2020 INRIA + */ + +#ifndef __eigenpy_decompositions_generalized_eigen_solver_hpp__ +#define __eigenpy_decompositions_generalized_eigen_solver_hpp__ + +#include +#include + +#include "eigenpy/eigen-to-python.hpp" +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" + +namespace eigenpy { + +template +struct GeneralizedEigenSolverVisitor + : public boost::python::def_visitor< + GeneralizedEigenSolverVisitor<_MatrixType>> { + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef Eigen::GeneralizedEigenSolver Solver; + + template + void visit(PyClass& cl) const { + cl.def(bp::init<>("Default constructor")) + .def(bp::init( + bp::arg("size"), "Default constructor with memory preallocation. ")) + .def(bp::init>( + bp::args("A", "B", "computeEigenVectors"), + "Computes the generalized eigendecomposition of given matrix " + "pair. ")) + + .def("alphas", &Solver::alphas, bp::arg("self"), + "Returns the vectors containing the alpha values ") + .def("betas", &Solver::betas, bp::arg("self"), + "Returns the vectors containing the beta values ") + + .def("compute", + &GeneralizedEigenSolverVisitor::compute_proxy, + bp::args("self", "A", "B"), + "Computes generalized eigendecomposition of given matrix. ", + bp::return_self<>()) + .def("compute", + (Solver & + (Solver::*)(const MatrixType& A, const MatrixType& B, bool)) & + Solver::compute, + bp::args("self", "A", "B", "computeEigenvectors"), + "Computes generalized eigendecomposition of given matrix. .", + bp::return_self<>()) + + .def("eigenvectors", &Solver::eigenvectors, bp::arg("self"), + "Returns an expression of the computed generalized eigenvectors. ") + + .def("info", &Solver::info, bp::arg("self"), + "NumericalIssue if the input contains INF or NaN values or " + "overflow occured. Returns Success otherwise.") + + .def("setMaxIterations", &Solver::setMaxIterations, + bp::args("self", "max_iter"), + "Sets the maximum number of iterations allowed.", + bp::return_self<>()); + } + + static void expose() { + static const std::string classname = + "GeneralizedEigenSolver" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string& name) { + bp::class_(name.c_str(), bp::no_init) + .def(GeneralizedEigenSolverVisitor()) + .def(IdVisitor()); + } + + private: + template + static Solver& compute_proxy(Solver& self, const MatrixType& A, + const MatrixType& B) { + return self.compute(A, B); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_generalized_eigen_solver_hpp__ diff --git a/include/eigenpy/decompositions/GeneralizedSelfAdjointEigenSolver.hpp b/include/eigenpy/decompositions/GeneralizedSelfAdjointEigenSolver.hpp new file mode 100644 index 000000000..274d14c1b --- /dev/null +++ b/include/eigenpy/decompositions/GeneralizedSelfAdjointEigenSolver.hpp @@ -0,0 +1,75 @@ +/* + * Copyright 2020 INRIA + */ + +#ifndef __eigenpy_decompositions_generalized_self_adjoint_eigen_solver_hpp__ +#define __eigenpy_decompositions_generalized_self_adjoint_eigen_solver_hpp__ + +#include +#include + +#include "eigenpy/eigen-to-python.hpp" +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" +#include "eigenpy/decompositions/SelfAdjointEigenSolver.hpp" + +namespace eigenpy { + +template +struct GeneralizedSelfAdjointEigenSolverVisitor + : public boost::python::def_visitor< + GeneralizedSelfAdjointEigenSolverVisitor<_MatrixType>> { + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef Eigen::GeneralizedSelfAdjointEigenSolver Solver; + + template + void visit(PyClass& cl) const { + cl.def(bp::init<>("Default constructor")) + .def(bp::init( + bp::arg("size"), "Default constructor with memory preallocation. ")) + .def(bp::init>( + bp::args("matA", "matB", "options"), + "Constructor: Computes generalized eigendecomposition of given " + "matrix pencil. ")) + + .def("compute", + &GeneralizedSelfAdjointEigenSolverVisitor::compute_proxy< + MatrixType>, + bp::args("self", "A", "B"), + "Computes generalized eigendecomposition of given matrix pencil. ", + bp::return_self<>()) + .def("compute", + (Solver & + (Solver::*)(const MatrixType& A, const MatrixType& B, int)) & + Solver::compute, + bp::args("self", "A", "B", "options"), + "Computes generalized eigendecomposition of given matrix pencil.", + bp::return_self<>()); + } + + static void expose() { + static const std::string classname = + "GeneralizedSelfAdjointEigenSolver" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string& name) { + bp::class_>>( + name.c_str(), bp::no_init) + .def(GeneralizedSelfAdjointEigenSolverVisitor()) + .def(IdVisitor()); + } + + private: + template + static Solver& compute_proxy(Solver& self, const MatrixType& A, + const MatrixType& B) { + return self.compute(A, B); + } +}; + +} // namespace eigenpy + +#endif // ifndef + // __eigenpy_decompositions_generalized_self_adjoint_eigen_solver_hpp__ diff --git a/include/eigenpy/decompositions/HessenbergDecomposition.hpp b/include/eigenpy/decompositions/HessenbergDecomposition.hpp new file mode 100644 index 000000000..c1c6e3b66 --- /dev/null +++ b/include/eigenpy/decompositions/HessenbergDecomposition.hpp @@ -0,0 +1,82 @@ +/* + * Copyright 2020 INRIA + */ + +#ifndef __eigenpy_decompositions_hessenberg_decomposition_hpp__ +#define __eigenpy_decompositions_hessenberg_decomposition_hpp__ + +#include +#include + +#include "eigenpy/eigen-to-python.hpp" +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" + +namespace eigenpy { + +template +struct HessenbergDecompositionVisitor + : public boost::python::def_visitor< + HessenbergDecompositionVisitor<_MatrixType>> { + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef Eigen::HessenbergDecomposition Solver; + + template + void visit(PyClass& cl) const { + cl + .def(bp::init( + bp::arg("size"), + "Default constructor; the decomposition will be computed later. ")) + .def(bp::init( + bp::arg("matrix"), + "Constructor; computes Hessenberg decomposition of given matrix. ")) + + .def("compute", + &HessenbergDecompositionVisitor::compute_proxy, + bp::args("self", "A"), + "Computes Hessenberg decomposition of given matrix. ", + bp::return_self<>()) + .def( + "compute", + (Solver & (Solver::*)(const Eigen::EigenBase& matrix)) & + Solver::compute, + bp::args("self", "A"), + "Computes Hessenberg decomposition of given matrix. ", + bp::return_self<>()) + + .def("householderCoefficients", &Solver::householderCoefficients, + bp::arg("self"), "Returns the Householder coefficients. ", + bp::return_value_policy()) + + // matrixH + // matrixQ + + .def("packedMatrix", &Solver::packedMatrix, bp::arg("self"), + "Returns the internal representation of the decomposition. ", + bp::return_value_policy()); + } + + static void expose() { + static const std::string classname = + "HessenbergDecomposition" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string& name) { + bp::class_(name.c_str(), bp::no_init) + .def(HessenbergDecompositionVisitor()) + .def(IdVisitor()); + } + + private: + template + static Solver& compute_proxy(Solver& self, + const Eigen::EigenBase& matrix) { + return self.compute(matrix); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_hessenberg_decomposition_hpp__ diff --git a/include/eigenpy/decompositions/JacobiSVD.hpp b/include/eigenpy/decompositions/JacobiSVD.hpp index 9fa39958a..5381d4164 100644 --- a/include/eigenpy/decompositions/JacobiSVD.hpp +++ b/include/eigenpy/decompositions/JacobiSVD.hpp @@ -18,7 +18,6 @@ namespace eigenpy { template struct JacobiSVDVisitor : public boost::python::def_visitor> { - typedef _MatrixType MatrixType; typedef Eigen::JacobiSVD Solver; typedef typename MatrixType::Scalar Scalar; @@ -32,34 +31,39 @@ struct JacobiSVDVisitor .def(bp::init( bp::args("self", "rows", "cols", "computationOptions "), "Default Constructor with memory preallocation. \n\n" - "Like the default constructor but with preallocation of the internal " - "data according to the specified problem size and the computationOptions. ")) + "Like the default constructor but with preallocation of the " + "internal " + "data according to the specified problem size and the " + "computationOptions. ")) .def(bp::init( bp::args("self", "matrix"), "Constructor performing the decomposition of given matrix. ")) .def(bp::init( bp::args("self", "matrix", "computationOptions "), "Constructor performing the decomposition of given matrix. \n\n" - "One cannot request unitiaries using both the Options template parameter " - "and the constructor. If possible, prefer using the Options template parameter.")) + "One cannot request unitiaries using both the Options template " + "parameter " + "and the constructor. If possible, prefer using the Options " + "template parameter.")) .def("cols", &Solver::cols, bp::arg("self"), "Returns the number of columns. ") - .def( - "compute", - (Solver & (Solver::*)(const MatrixType &matrix)) & - Solver::compute, - bp::args("self", "matrix"), - "Method performing the decomposition of given matrix. Computes Thin/Full " - "unitaries U/V if specified using the Options template parameter or the class constructor. ", - bp::return_self<>()) - .def( - "compute", - (Solver & (Solver::*)(const MatrixType &matrix, unsigned int computationOptions)) & - Solver::compute, - bp::args("self", "matrix"), - "Method performing the decomposition of given matrix, as specified by the computationOptions parameter. ", - bp::return_self<>()) + .def("compute", + (Solver & (Solver::*)(const MatrixType &matrix)) & Solver::compute, + bp::args("self", "matrix"), + "Method performing the decomposition of given matrix. Computes " + "Thin/Full " + "unitaries U/V if specified using the Options template parameter " + "or the class constructor. ", + bp::return_self<>()) + .def("compute", + (Solver & (Solver::*)(const MatrixType &matrix, + unsigned int computationOptions)) & + Solver::compute, + bp::args("self", "matrix"), + "Method performing the decomposition of given matrix, as " + "specified by the computationOptions parameter. ", + bp::return_self<>()) .def("rows", &Solver::rows, bp::arg("self"), "Returns the number of rows. . ") @@ -76,20 +80,32 @@ struct JacobiSVDVisitor bp::class_( name.c_str(), "Two-sided Jacobi SVD decomposition of a rectangular matrix. \n\n" - "SVD decomposition consists in decomposing any n-by-p matrix A as a product " - "A=USV∗ where U is a n-by-n unitary, V is a p-by-p unitary, and S is a n-by-p r" - "eal positive matrix which is zero outside of its main diagonal; the diagonal " - "entries of S are known as the singular values of A and the columns of U and V " - "are known as the left and right singular vectors of A respectively. \n\n" + "SVD decomposition consists in decomposing any n-by-p matrix A as a " + "product " + "A=USV∗ where U is a n-by-n unitary, V is a p-by-p unitary, and S is a " + "n-by-p r" + "eal positive matrix which is zero outside of its main diagonal; the " + "diagonal " + "entries of S are known as the singular values of A and the columns of " + "U and V " + "are known as the left and right singular vectors of A respectively. " + "\n\n" "Singular values are always sorted in decreasing order. \n\n " - "This JacobiSVD decomposition computes only the singular values by default. " + "This JacobiSVD decomposition computes only the singular values by " + "default. " "If you want U or V, you need to ask for them explicitly. \n\n" - "You can ask for only thin U or V to be computed, meaning the following. " - "In case of a rectangular n-by-p matrix, letting m be the smaller value among " - "n and p, there are only m singular vectors; the remaining columns of U and V " - "do not correspond to actual singular vectors. Asking for thin U or V means asking " - "for only their m first columns to be formed. So U is then a n-by-m matrix, and V " - "is then a p-by-m matrix. Notice that thin U and V are all you need for (least squares) " + "You can ask for only thin U or V to be computed, meaning the " + "following. " + "In case of a rectangular n-by-p matrix, letting m be the smaller " + "value among " + "n and p, there are only m singular vectors; the remaining columns of " + "U and V " + "do not correspond to actual singular vectors. Asking for thin U or V " + "means asking " + "for only their m first columns to be formed. So U is then a n-by-m " + "matrix, and V " + "is then a p-by-m matrix. Notice that thin U and V are all you need " + "for (least squares) " "solving.", bp::no_init) .def(JacobiSVDVisitor()) diff --git a/include/eigenpy/decompositions/RealQZ.hpp b/include/eigenpy/decompositions/RealQZ.hpp new file mode 100644 index 000000000..1b339345f --- /dev/null +++ b/include/eigenpy/decompositions/RealQZ.hpp @@ -0,0 +1,90 @@ +/* + * Copyright 2020 INRIA + */ + +#ifndef __eigenpy_decompositions_generalized_real_qz_hpp__ +#define __eigenpy_decompositions_generalized_real_qz_hpp__ + +#include +#include + +#include "eigenpy/eigen-to-python.hpp" +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" + +namespace eigenpy { + +template +struct RealQZVisitor + : public boost::python::def_visitor> { + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef Eigen::RealQZ Solver; + + template + void visit(PyClass& cl) const { + cl.def( + bp::init(bp::arg("size"), "Default constructor. ")) + .def(bp::init>( + bp::args("A", "B", "computeQZ"), + "Constructor; computes real QZ decomposition of given matrices. ")) + + .def("compute", &RealQZVisitor::compute_proxy, + bp::args("self", "A", "B"), + "Computes QZ decomposition of given matrix. ", + bp::return_self<>()) + .def("compute", + (Solver & + (Solver::*)(const MatrixType& A, const MatrixType& B, bool)) & + Solver::compute, + bp::args("self", "A", "B", "computeEigenvectors"), + "Computes QZ decomposition of given matrix. ", bp::return_self<>()) + + .def("info", &Solver::info, bp::arg("self"), + "NumericalIssue if the input contains INF or NaN values or " + "overflow occured. Returns Success otherwise.") + + .def("matrixQ", &Solver::matrixQ, bp::arg("self"), + "Returns matrix Q in the QZ decomposition. ", + bp::return_value_policy()) + .def("matrixS", &Solver::matrixS, bp::arg("self"), + "Returns matrix S in the QZ decomposition. ", + bp::return_value_policy()) + .def("matrixT", &Solver::matrixT, bp::arg("self"), + "Returns matrix T in the QZ decomposition. ", + bp::return_value_policy()) + .def("matrixZ", &Solver::matrixZ, bp::arg("self"), + "Returns matrix Z in the QZ decomposition. ", + bp::return_value_policy()) + + .def("iterations", &Solver::iterations, bp::arg("self"), + "Returns number of performed QR-like iterations. ") + .def("setMaxIterations", &Solver::setMaxIterations, + bp::args("self", "max_iter"), + "Sets the maximum number of iterations allowed.", + bp::return_self<>()); + } + + static void expose() { + static const std::string classname = + "RealQZVisitor" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string& name) { + bp::class_(name.c_str(), bp::no_init) + .def(RealQZVisitor()) + .def(IdVisitor()); + } + + private: + template + static Solver& compute_proxy(Solver& self, const MatrixType& A, + const MatrixType& B) { + return self.compute(A, B); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_generalized_real_qz_hpp__ diff --git a/include/eigenpy/decompositions/RealSchur.hpp b/include/eigenpy/decompositions/RealSchur.hpp new file mode 100644 index 000000000..24dcf0cb9 --- /dev/null +++ b/include/eigenpy/decompositions/RealSchur.hpp @@ -0,0 +1,90 @@ +/* + * Copyright 2020 INRIA + */ + +#ifndef __eigenpy_decompositions_generalized_real_schur_hpp__ +#define __eigenpy_decompositions_generalized_real_schur_hpp__ + +#include +#include + +#include "eigenpy/eigen-to-python.hpp" +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" + +namespace eigenpy { + +template +struct RealSchurVisitor + : public boost::python::def_visitor> { + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef Eigen::RealSchur Solver; + + template + void visit(PyClass& cl) const { + cl.def( + bp::init(bp::arg("size"), "Default constructor. ")) + .def(bp::init>( + bp::args("matrix", "computeU"), + "Constructor; computes real Schur decomposition of given matrix. ")) + + .def("compute", &RealSchurVisitor::compute_proxy, + bp::args("self", "matrix"), + "Computes Schur decomposition of given matrix. ", + bp::return_self<>()) + .def("compute", + (Solver & + (Solver::*)(const Eigen::EigenBase& matrix, bool)) & + Solver::compute, + bp::args("self", "matrix", "computeEigenvectors"), + "Computes Schur decomposition of given matrix. ", + bp::return_self<>()) + + .def("computeFromHessenberg", + (Solver & (Solver::*)(const MatrixType& matrixH, + const MatrixType& matrixQ, bool)) & + Solver::computeFromHessenberg, + bp::args("self", "matrixH", "matrixQ", "computeU"), + "Compute Schur decomposition from a given Hessenberg matrix. ", + bp::return_self<>()) + + .def("info", &Solver::info, bp::arg("self"), + "NumericalIssue if the input contains INF or NaN values or " + "overflow occured. Returns Success otherwise.") + + .def("matrixT", &Solver::matrixT, bp::arg("self"), + "Returns the quasi-triangular matrix in the Schur decomposition.", + bp::return_value_policy()) + .def("matrixU", &Solver::matrixU, bp::arg("self"), + "Returns the orthogonal matrix in the Schur decomposition. ", + bp::return_value_policy()) + + .def("setMaxIterations", &Solver::setMaxIterations, + bp::args("self", "max_iter"), + "Sets the maximum number of iterations allowed.", + bp::return_self<>()); + } + + static void expose() { + static const std::string classname = + "RealSchurVisitor" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string& name) { + bp::class_(name.c_str(), bp::no_init) + .def(RealSchurVisitor()) + .def(IdVisitor()); + } + + private: + template + static Solver& compute_proxy(Solver& self, const MatrixType& A) { + return self.compute(A); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_generalized_real_schur_hpp__ diff --git a/include/eigenpy/decompositions/Tridiagonalization.hpp b/include/eigenpy/decompositions/Tridiagonalization.hpp new file mode 100644 index 000000000..2b5f1177c --- /dev/null +++ b/include/eigenpy/decompositions/Tridiagonalization.hpp @@ -0,0 +1,83 @@ +/* + * Copyright 2020 INRIA + */ + +#ifndef __eigenpy_decompositions_tridiagonalization_hpp__ +#define __eigenpy_decompositions_tridiagonalization_hpp__ + +#include +#include + +#include "eigenpy/eigen-to-python.hpp" +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" + +namespace eigenpy { + +template +struct TridiagonalizationVisitor + : public boost::python::def_visitor> { + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef Eigen::Tridiagonalization Solver; + + template + void visit(PyClass& cl) const { + cl.def( + bp::init(bp::arg("size"), "Default constructor. ")) + .def(bp::init( + bp::arg("matrix"), + "Constructor; computes tridiagonal decomposition of given matrix. ")) + + .def("compute", &TridiagonalizationVisitor::compute_proxy, + bp::args("self", "matrix"), + "Computes tridiagonal decomposition of given matrix. ", + bp::return_self<>()) + .def("compute", + (Solver & + (Solver::*)(const Eigen::EigenBase& matrix)) & + Solver::compute, + bp::args("self", "matrix"), + "Computes tridiagonal decomposition of given matrix. ", bp::return_self<>()) + + .def("diagonal", &Solver::diagonal, bp::arg("self"), + "Returns the diagonal of the tridiagonal matrix T in the decomposition. ") + + .def("householderCoefficients", &Solver::householderCoefficients, + bp::arg("self"), "Returns the Householder coefficients. ") + + .def("matrixQ", &Solver::matrixQ, + bp::arg("self"), "Returns the unitary matrix Q in the decomposition. ") + .def("matrixT", &Solver::matrixT, + bp::arg("self"), "Returns the unitary matrix T in the decomposition. ") + + .def("packedMatrix", &Solver::packedMatrix, bp::arg("self"), + "Returns the internal representation of the decomposition. ", + bp::return_value_policy()) + + .def("subDiagonal", &Solver::subDiagonal, bp::arg("self"), + "Returns the subdiagonal of the tridiagonal matrix T in the decomposition."); + } + + static void expose() { + static const std::string classname = + "TridiagonalizationVisitor" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string& name) { + bp::class_(name.c_str(), bp::no_init) + .def(TridiagonalizationVisitor()) + .def(IdVisitor()); + } + + private: + template + static Solver& compute_proxy(Solver& self, const Eigen::EigenBase& matrix) { + return self.compute(matrix); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_tridiagonalization_hpp__ diff --git a/src/decompositions/complex-eigen-solver.cpp b/src/decompositions/complex-eigen-solver.cpp new file mode 100644 index 000000000..4bef19df5 --- /dev/null +++ b/src/decompositions/complex-eigen-solver.cpp @@ -0,0 +1,13 @@ + +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/ComplexEigenSolver.hpp" + +namespace eigenpy { +void exposeComplexEigenSolver() { + using namespace Eigen; + ComplexEigenSolverVisitor::expose("ComplexEigenSolver"); +} +} // namespace eigenpy diff --git a/src/decompositions/complex-schur.cpp b/src/decompositions/complex-schur.cpp new file mode 100644 index 000000000..ece7c1954 --- /dev/null +++ b/src/decompositions/complex-schur.cpp @@ -0,0 +1,13 @@ + +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/ComplexSchur.hpp" + +namespace eigenpy { +void exposeComplexSchur() { + using namespace Eigen; + ComplexSchurVisitor::expose("ComplexSchur"); +} +} // namespace eigenpy diff --git a/src/decompositions/decompositions.cpp b/src/decompositions/decompositions.cpp index 5f0933a09..42197f0fd 100644 --- a/src/decompositions/decompositions.cpp +++ b/src/decompositions/decompositions.cpp @@ -9,7 +9,15 @@ namespace eigenpy { void exposeEigenSolver(); +void exposeGeneralizedEigenSolver(); void exposeSelfAdjointEigenSolver(); +void exposeGeneralizedSelfAdjointEigenSolver(); +void exposeHessenbergDecomposition(); +void exposeRealQZ(); +void exposeRealSchur(); +void exposeTridiagonalization(); +void exposeComplexEigenSolver(); +void exposeComplexSchur(); void exposeLLTSolver(); void exposeLDLTSolver(); void exposeFullPivLUSolver(); @@ -28,7 +36,15 @@ void exposeDecompositions() { using namespace Eigen; exposeEigenSolver(); + exposeGeneralizedEigenSolver(); exposeSelfAdjointEigenSolver(); + exposeGeneralizedSelfAdjointEigenSolver(); + exposeHessenbergDecomposition(); + exposeRealQZ(); + exposeRealSchur(); + exposeTridiagonalization(); + exposeComplexEigenSolver(); + exposeComplexSchur(); exposeLLTSolver(); exposeLDLTSolver(); exposeFullPivLUSolver(); diff --git a/src/decompositions/generalized-eigen-solver.cpp b/src/decompositions/generalized-eigen-solver.cpp new file mode 100644 index 000000000..6b8a422ac --- /dev/null +++ b/src/decompositions/generalized-eigen-solver.cpp @@ -0,0 +1,13 @@ + +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/GeneralizedEigenSolver.hpp" + +namespace eigenpy { +void exposeGeneralizedEigenSolver() { + using namespace Eigen; + GeneralizedEigenSolverVisitor::expose("GeneralizedEigenSolver"); +} +} // namespace eigenpy diff --git a/src/decompositions/generalized-self-adjoint-eigen-solver.cpp b/src/decompositions/generalized-self-adjoint-eigen-solver.cpp new file mode 100644 index 000000000..ac5330223 --- /dev/null +++ b/src/decompositions/generalized-self-adjoint-eigen-solver.cpp @@ -0,0 +1,14 @@ + +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/GeneralizedSelfAdjointEigenSolver.hpp" + +namespace eigenpy { +void exposeGeneralizedSelfAdjointEigenSolver() { + using namespace Eigen; + GeneralizedSelfAdjointEigenSolverVisitor::expose( + "GeneralizedSelfAdjointEigenSolver"); +} +} // namespace eigenpy diff --git a/src/decompositions/hessenberg-decomposition.cpp b/src/decompositions/hessenberg-decomposition.cpp new file mode 100644 index 000000000..9301f324c --- /dev/null +++ b/src/decompositions/hessenberg-decomposition.cpp @@ -0,0 +1,13 @@ + +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/HessenbergDecomposition.hpp" + +namespace eigenpy { +void exposeHessenbergDecomposition() { + using namespace Eigen; + HessenbergDecompositionVisitor::expose("HessenbergDecomposition"); +} +} // namespace eigenpy diff --git a/src/decompositions/real-qz.cpp b/src/decompositions/real-qz.cpp new file mode 100644 index 000000000..2920024e3 --- /dev/null +++ b/src/decompositions/real-qz.cpp @@ -0,0 +1,13 @@ + +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/RealQZ.hpp" + +namespace eigenpy { +void exposeRealQZ() { + using namespace Eigen; + RealQZVisitor::expose("RealQZ"); +} +} // namespace eigenpy diff --git a/src/decompositions/real-schur.cpp b/src/decompositions/real-schur.cpp new file mode 100644 index 000000000..05f8b2402 --- /dev/null +++ b/src/decompositions/real-schur.cpp @@ -0,0 +1,13 @@ + +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/RealSchur.hpp" + +namespace eigenpy { +void exposeRealSchur() { + using namespace Eigen; + RealSchurVisitor::expose("RealSchur"); +} +} // namespace eigenpy diff --git a/src/decompositions/tridiagonalization.cpp b/src/decompositions/tridiagonalization.cpp new file mode 100644 index 000000000..25e1e3185 --- /dev/null +++ b/src/decompositions/tridiagonalization.cpp @@ -0,0 +1,13 @@ + +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/Tridiagonalization.hpp" + +namespace eigenpy { +void exposeTridiagonalization() { + using namespace Eigen; + TridiagonalizationVisitor::expose("Tridiagonalization"); +} +} // namespace eigenpy diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 0df3622df..90eb2a97d 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -137,10 +137,36 @@ add_python_eigenpy_lib_unit_test("py-version" "unittest/python/test_version.py") add_python_eigenpy_lib_unit_test("py-eigen-solver" "unittest/python/test_eigen_solver.py") +add_python_eigenpy_lib_unit_test( + "py-generalized-eigen-solver" + "unittest/python/test_generalized_eigen_solver.py") + +add_python_eigenpy_lib_unit_test( + "py-generalized-self-adjoint-eigen-solver" + "unittest/python/test_generalized_self_adjoint_eigen_solver.py") + +add_python_eigenpy_lib_unit_test("py-complex-eigen-solver" + "unittest/python/test_complex_eigen_solver.py") + +add_python_eigenpy_lib_unit_test("py-complex-schur" + "unittest/python/test_complex_schur.py") + add_python_eigenpy_lib_unit_test( "py-self-adjoint-eigen-solver" "unittest/python/test_self_adjoint_eigen_solver.py") +add_python_eigenpy_lib_unit_test( + "py-hessenberg-decomposition" + "unittest/python/test_hessenberg_decomposition.py") + +add_python_eigenpy_lib_unit_test("py-real-qz" "unittest/python/test_real_qz.py") + +add_python_eigenpy_lib_unit_test("py-real-schur" + "unittest/python/test_real_schur.py") + +add_python_eigenpy_lib_unit_test("py-tridiagonalization" + "unittest/python/test_tridiagonalization.py") + add_python_eigenpy_lib_unit_test("py-LLT" "unittest/python/test_LLT.py") add_python_eigenpy_lib_unit_test("py-LDLT" "unittest/python/test_LDLT.py") diff --git a/unittest/python/test_complex_eigen_solver.py b/unittest/python/test_complex_eigen_solver.py new file mode 100644 index 000000000..4790e0e52 --- /dev/null +++ b/unittest/python/test_complex_eigen_solver.py @@ -0,0 +1,15 @@ +import numpy as np + +import eigenpy + +dim = 100 +rng = np.random.default_rng() +A = rng.random((dim, dim)) + +ces = eigenpy.ComplexEigenSolver(A) + +V = ces.eigenvectors() +D = ces.eigenvalues() + +assert eigenpy.is_approx(A.dot(V).real, V.dot(np.diag(D)).real) +assert eigenpy.is_approx(A.dot(V).imag, V.dot(np.diag(D)).imag) diff --git a/unittest/python/test_complex_schur.py b/unittest/python/test_complex_schur.py new file mode 100644 index 000000000..1a9cd2e4c --- /dev/null +++ b/unittest/python/test_complex_schur.py @@ -0,0 +1,16 @@ +import numpy as np + +import eigenpy + +dim = 100 +rng = np.random.default_rng() +A = rng.random((dim, dim)) + +cs = eigenpy.ComplexSchur(A) + +U = cs.matrixU() +T = cs.matrixT() +U_star = U.conj().T + +assert eigenpy.is_approx(A.real, (U @ T @ U_star).real) +assert np.allclose(A.imag, (U @ T @ U_star).imag, atol=1e-10) diff --git a/unittest/python/test_generalized_eigen_solver.py b/unittest/python/test_generalized_eigen_solver.py new file mode 100644 index 000000000..6fcc3ca0d --- /dev/null +++ b/unittest/python/test_generalized_eigen_solver.py @@ -0,0 +1,26 @@ +import numpy as np + +import eigenpy + +dim = 100 +rng = np.random.default_rng() +A = rng.random((dim, dim)) +B = rng.random((dim, dim)) + +ges = eigenpy.GeneralizedEigenSolver(A, B) + +alphas = ges.alphas() +betas = ges.betas() +V = ges.eigenvectors() + +eigenvalues = alphas / betas +if np.all(np.abs(betas) >= 1e-15): + for i in range(dim): + v = V[:, i] + lam = eigenvalues[i] + + Av = A @ v + lam_Bv = lam * (B @ v) + + assert eigenpy.is_approx(Av.real, lam_Bv.real) + assert eigenpy.is_approx(Av.imag, lam_Bv.imag) diff --git a/unittest/python/test_generalized_self_adjoint_eigen_solver.py b/unittest/python/test_generalized_self_adjoint_eigen_solver.py new file mode 100644 index 000000000..2658c777d --- /dev/null +++ b/unittest/python/test_generalized_self_adjoint_eigen_solver.py @@ -0,0 +1,26 @@ +import numpy as np + +import eigenpy + +dim = 5 +rng = np.random.default_rng() + +A = rng.random((dim, dim)) +A = (A + A.T) * 0.5 + +B = rng.random((dim, dim)) +B = B @ B.T + 0.1 * np.eye(dim) + +gsaes = eigenpy.GeneralizedSelfAdjointEigenSolver(A, B) + +V = gsaes.eigenvectors() +D = gsaes.eigenvalues() + +for i in range(dim): + v = V[:, i] + lam = D[i] + + Av = A @ v + lam_Bv = lam * (B @ v) + + assert np.allclose(Av, lam_Bv, atol=1e-6) diff --git a/unittest/python/test_hessenberg_decomposition.py b/unittest/python/test_hessenberg_decomposition.py new file mode 100644 index 000000000..4e1662dd3 --- /dev/null +++ b/unittest/python/test_hessenberg_decomposition.py @@ -0,0 +1,9 @@ +import numpy as np + +import eigenpy + +dim = 100 +rng = np.random.default_rng() +A = rng.random((dim, dim)) + +ges = eigenpy.HessenbergDecomposition(A) diff --git a/unittest/python/test_real_qz.py b/unittest/python/test_real_qz.py new file mode 100644 index 000000000..cf1fbd1c3 --- /dev/null +++ b/unittest/python/test_real_qz.py @@ -0,0 +1,18 @@ +import numpy as np + +import eigenpy + +dim = 100 +rng = np.random.default_rng() +A = rng.random((dim, dim)) +B = rng.random((dim, dim)) + +realqz = eigenpy.RealQZ(A, B) + +Q = realqz.matrixQ() +S = realqz.matrixS() +Z = realqz.matrixZ() +T = realqz.matrixT() + +assert eigenpy.is_approx(A, Q @ S @ Z) +assert eigenpy.is_approx(B, Q @ T @ Z) diff --git a/unittest/python/test_real_schur.py b/unittest/python/test_real_schur.py new file mode 100644 index 000000000..a3f5e2811 --- /dev/null +++ b/unittest/python/test_real_schur.py @@ -0,0 +1,14 @@ +import numpy as np + +import eigenpy + +dim = 100 +rng = np.random.default_rng() +A = rng.random((dim, dim)) + +cs = eigenpy.RealSchur(A) + +U = cs.matrixU() +T = cs.matrixT() + +assert eigenpy.is_approx(A.real, (U @ T @ U.T).real) diff --git a/unittest/python/test_tridiagonalization.py b/unittest/python/test_tridiagonalization.py new file mode 100644 index 000000000..bdf86aa7f --- /dev/null +++ b/unittest/python/test_tridiagonalization.py @@ -0,0 +1,20 @@ +import numpy as np + +import eigenpy + +dim = 100 +rng = np.random.default_rng() +A = rng.random((dim, dim)) + +tri = eigenpy.Tridiagonalization(A) + +# Q = tri.matrixQ() +# print("Q") +# print(Q) +# Q_conj = Q.conj().T + +# T = tri.matrixT() +# print("T") +# print(T) + +# assert eigenpy.is_approx(A, Q @ T @ Q_conj) From c8cb13eb16f8577868f15354ea549c1816ef97ed Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Fri, 4 Jul 2025 17:16:22 +0200 Subject: [PATCH 05/43] solvers: BiCGSTAB --- CMakeLists.txt | 1 + .../decompositions/Tridiagonalization.hpp | 40 ++++++++++-------- include/eigenpy/solvers/BiCGSTAB.hpp | 41 +++++++++++++++++++ src/solvers/solvers.cpp | 5 +++ 4 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 include/eigenpy/solvers/BiCGSTAB.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 228dd128f..4d665dc08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -190,6 +190,7 @@ set(${PROJECT_NAME}_SOLVERS_HEADERS include/eigenpy/solvers/preconditioners.hpp include/eigenpy/solvers/IterativeSolverBase.hpp include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp + include/eigenpy/solvers/BiCGSTAB.hpp include/eigenpy/solvers/ConjugateGradient.hpp include/eigenpy/solvers/SparseSolverBase.hpp include/eigenpy/solvers/BasicPreconditioners.hpp diff --git a/include/eigenpy/decompositions/Tridiagonalization.hpp b/include/eigenpy/decompositions/Tridiagonalization.hpp index 2b5f1177c..4566b3172 100644 --- a/include/eigenpy/decompositions/Tridiagonalization.hpp +++ b/include/eigenpy/decompositions/Tridiagonalization.hpp @@ -15,8 +15,8 @@ namespace eigenpy { template -struct TridiagonalizationVisitor - : public boost::python::def_visitor> { +struct TridiagonalizationVisitor : public boost::python::def_visitor< + TridiagonalizationVisitor<_MatrixType>> { typedef _MatrixType MatrixType; typedef typename MatrixType::Scalar Scalar; typedef Eigen::Tridiagonalization Solver; @@ -25,38 +25,41 @@ struct TridiagonalizationVisitor void visit(PyClass& cl) const { cl.def( bp::init(bp::arg("size"), "Default constructor. ")) - .def(bp::init( - bp::arg("matrix"), - "Constructor; computes tridiagonal decomposition of given matrix. ")) + .def(bp::init(bp::arg("matrix"), + "Constructor; computes tridiagonal " + "decomposition of given matrix. ")) .def("compute", &TridiagonalizationVisitor::compute_proxy, bp::args("self", "matrix"), "Computes tridiagonal decomposition of given matrix. ", bp::return_self<>()) - .def("compute", - (Solver & - (Solver::*)(const Eigen::EigenBase& matrix)) & - Solver::compute, - bp::args("self", "matrix"), - "Computes tridiagonal decomposition of given matrix. ", bp::return_self<>()) + .def( + "compute", + (Solver & (Solver::*)(const Eigen::EigenBase& matrix)) & + Solver::compute, + bp::args("self", "matrix"), + "Computes tridiagonal decomposition of given matrix. ", + bp::return_self<>()) .def("diagonal", &Solver::diagonal, bp::arg("self"), - "Returns the diagonal of the tridiagonal matrix T in the decomposition. ") + "Returns the diagonal of the tridiagonal matrix T in the " + "decomposition. ") .def("householderCoefficients", &Solver::householderCoefficients, bp::arg("self"), "Returns the Householder coefficients. ") - .def("matrixQ", &Solver::matrixQ, - bp::arg("self"), "Returns the unitary matrix Q in the decomposition. ") - .def("matrixT", &Solver::matrixT, - bp::arg("self"), "Returns the unitary matrix T in the decomposition. ") + .def("matrixQ", &Solver::matrixQ, bp::arg("self"), + "Returns the unitary matrix Q in the decomposition. ") + .def("matrixT", &Solver::matrixT, bp::arg("self"), + "Returns the unitary matrix T in the decomposition. ") .def("packedMatrix", &Solver::packedMatrix, bp::arg("self"), "Returns the internal representation of the decomposition. ", bp::return_value_policy()) .def("subDiagonal", &Solver::subDiagonal, bp::arg("self"), - "Returns the subdiagonal of the tridiagonal matrix T in the decomposition."); + "Returns the subdiagonal of the tridiagonal matrix T in the " + "decomposition."); } static void expose() { @@ -73,7 +76,8 @@ struct TridiagonalizationVisitor private: template - static Solver& compute_proxy(Solver& self, const Eigen::EigenBase& matrix) { + static Solver& compute_proxy(Solver& self, + const Eigen::EigenBase& matrix) { return self.compute(matrix); } }; diff --git a/include/eigenpy/solvers/BiCGSTAB.hpp b/include/eigenpy/solvers/BiCGSTAB.hpp new file mode 100644 index 000000000..ae087da33 --- /dev/null +++ b/include/eigenpy/solvers/BiCGSTAB.hpp @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2018 CNRS + */ + +#ifndef __eigenpy_bicgstab_hpp__ +#define __eigenpy_bicgstab_hpp__ + +#include + +#include "eigenpy/fwd.hpp" +#include "eigenpy/solvers/IterativeSolverBase.hpp" + +namespace eigenpy { + +template +struct BiCGSTABVisitor + : public boost::python::def_visitor> { + typedef Eigen::MatrixXd MatrixType; + + template + void visit(PyClass& cl) const { + cl.def(bp::init<>("Default constructor")) + .def(bp::init( + bp::arg("A"), + "Initialize the solver with matrix A for further || Ax - b || " + "solving.\n" + "This constructor is a shortcut for the default constructor " + "followed by a call to compute().")); + } + + static void expose() { + bp::class_("BiCGSTAB" , bp::no_init) + .def(IterativeSolverVisitor()) + .def(BiCGSTABVisitor()) + .def(IdVisitor()); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_bicgstab_hpp__ diff --git a/src/solvers/solvers.cpp b/src/solvers/solvers.cpp index 207eb6e76..4c1fcad62 100644 --- a/src/solvers/solvers.cpp +++ b/src/solvers/solvers.cpp @@ -13,6 +13,8 @@ #include "eigenpy/solvers/LeastSquaresConjugateGradient.hpp" #endif +#include "eigenpy/solvers/BiCGSTAB.hpp" + namespace eigenpy { void exposeSolvers() { using namespace Eigen; @@ -30,6 +32,9 @@ void exposeSolvers() { // ConjugateGradientVisitor< // ConjugateGradient // > >::expose("LimitedBFGSConjugateGradient"); + + BiCGSTABVisitor>>::expose(); } } // namespace eigenpy From b67c6859c6904bb561377a99b584ff69e12397da Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Sun, 6 Jul 2025 22:00:12 +0200 Subject: [PATCH 06/43] unittest: Comments about permutationP and permutationQ in FullPivLU --- include/eigenpy/decompositions/FullPivLU.hpp | 16 ++++++++++------ include/eigenpy/solvers/BiCGSTAB.hpp | 2 +- unittest/python/test_FullPivLU.py | 17 +++++++++++------ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/include/eigenpy/decompositions/FullPivLU.hpp b/include/eigenpy/decompositions/FullPivLU.hpp index e95666db5..a6e8e4a40 100644 --- a/include/eigenpy/decompositions/FullPivLU.hpp +++ b/include/eigenpy/decompositions/FullPivLU.hpp @@ -90,6 +90,7 @@ struct FullPivLUSolverVisitor .def("maxPivot", &Solver::maxPivot, bp::arg("self")) .def("nonzeroPivots", &Solver::nonzeroPivots, bp::arg("self")) + // TODO: Expose so that the return type are convertible to np arrays .def("permutationP", &Solver::permutationP, bp::arg("self"), "Returns the permutation P.", bp::return_value_policy()) @@ -129,12 +130,15 @@ struct FullPivLUSolverVisitor "is strictly greater than |pivot| ⩽ threshold×|maxpivot| where " "maxpivot is the biggest pivot.", bp::return_self<>()) - .def("setThreshold", - (Solver & (Solver::*)(Eigen::Default_t)) & Solver::setThreshold, - bp::args("self", "threshold"), - "Allows to come back to the default behavior, letting Eigen use " - " its default formula for determining the threshold.", - bp::return_self<>()) + .def( + "setThreshold", + +[](Solver &self) -> Solver & { + return self.setThreshold(Eigen::Default); + }, + bp::arg("self"), + "Allows to come back to the default behavior, letting Eigen use " + "its default formula for determining the threshold.", + bp::return_self<>()) .def("solve", &solve, bp::args("self", "b"), "Returns the solution x of A x = b using the current " diff --git a/include/eigenpy/solvers/BiCGSTAB.hpp b/include/eigenpy/solvers/BiCGSTAB.hpp index ae087da33..3ac824992 100644 --- a/include/eigenpy/solvers/BiCGSTAB.hpp +++ b/include/eigenpy/solvers/BiCGSTAB.hpp @@ -29,7 +29,7 @@ struct BiCGSTABVisitor } static void expose() { - bp::class_("BiCGSTAB" , bp::no_init) + bp::class_("BiCGSTAB", bp::no_init) .def(IterativeSolverVisitor()) .def(BiCGSTABVisitor()) .def(IdVisitor()); diff --git a/unittest/python/test_FullPivLU.py b/unittest/python/test_FullPivLU.py index c9dc76c37..bb0ed52b5 100644 --- a/unittest/python/test_FullPivLU.py +++ b/unittest/python/test_FullPivLU.py @@ -32,15 +32,20 @@ is_invertible = fullpivlu.isInvertible() is_surjective = fullpivlu.isSurjective() -LU = fullpivlu.matrixLU() -permutationP = fullpivlu.permutationP() -permutationQ = fullpivlu.permutationQ() - -reconstructed_matrix = fullpivlu.reconstructedMatrix() - fullpivlu.setThreshold(1e-8) threshold = fullpivlu.threshold() +fullpivlu.setThreshold() +threshold = fullpivlu.threshold() + image = fullpivlu.image(A) inverse = fullpivlu.inverse() kernel = fullpivlu.kernel() + +LU = fullpivlu.matrixLU() +P = fullpivlu.permutationP() +Q = fullpivlu.permutationQ() + +reconstructed_matrix = fullpivlu.reconstructedMatrix() + +assert eigenpy.is_approx(reconstructed_matrix, A) From 9c809b26355b7687c79560c47a668ee7ee618214 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Sun, 6 Jul 2025 22:12:21 +0200 Subject: [PATCH 07/43] unittest: PartialPivLU reconstructedMatrix to retrieve the original matrix --- .../eigenpy/decompositions/PartialPivLU.hpp | 56 ++++++++++++------- unittest/python/test_FullPivLU.py | 1 - unittest/python/test_PartialPivLU.py | 4 +- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/include/eigenpy/decompositions/PartialPivLU.hpp b/include/eigenpy/decompositions/PartialPivLU.hpp index de0f6a4ea..d0460e08b 100644 --- a/include/eigenpy/decompositions/PartialPivLU.hpp +++ b/include/eigenpy/decompositions/PartialPivLU.hpp @@ -82,27 +82,41 @@ struct PartialPivLUSolverVisitor : public boost::python::def_visitor< } static void expose(const std::string &name) { - bp::class_(name.c_str(), - "LU decomposition of a matrix with partial pivoting, " - "and related features. \n\n" - "This class represents a LU decomposition of a square " - "invertible matrix, " - "with partial pivoting: the matrix A is decomposed as A " - "= PLU where L is " - "unit-lower-triangular, U is upper-triangular, and P is " - "a permutation matrix.\n\n" - "Typically, partial pivoting LU decomposition is only " - "considered numerically " - "stable for square invertible matrices. Thus LAPACK's " - "dgesv and dgesvx require " - "the matrix to be square and invertible. The present " - "class does the same. It " - "will assert that the matrix is square, but it won't " - "(actually it can't) check " - "that the matrix is invertible: it is your task to " - "check that you only use this " - "decomposition on invertible matrices.", - bp::no_init) + bp::class_( + name.c_str(), + "LU decomposition of a matrix with partial pivoting, " + "and related features. \n\n" + "This class represents a LU decomposition of a square " + "invertible matrix, " + "with partial pivoting: the matrix A is decomposed as A " + "= PLU where L is " + "unit-lower-triangular, U is upper-triangular, and P is " + "a permutation matrix.\n\n" + "Typically, partial pivoting LU decomposition is only " + "considered numerically " + "stable for square invertible matrices. Thus LAPACK's " + "dgesv and dgesvx require " + "the matrix to be square and invertible. The present " + "class does the same. It " + "will assert that the matrix is square, but it won't " + "(actually it can't) check " + "that the matrix is invertible: it is your task to " + "check that you only use this " + "decomposition on invertible matrices. \n\n" + "The guaranteed safe alternative, working for all matrices, " + "is the full pivoting LU decomposition, provided by class " + "FullPivLU. \n\n" + "This is not a rank-revealing LU decomposition. Many features " + "are intentionally absent from this class, such as " + "rank computation. If you need these features, use class " + "FullPivLU. \n\n" + "This LU decomposition is suitable to invert invertible " + "matrices. It is what MatrixBase::inverse() uses in the " + "general case. On the other hand, it is not suitable to " + "determine whether a given matrix is invertible. \n\n" + "The data of the LU decomposition can be directly accessed " + "through the methods matrixLU(), permutationP().", + bp::no_init) .def(IdVisitor()) .def(PartialPivLUSolverVisitor()); } diff --git a/unittest/python/test_FullPivLU.py b/unittest/python/test_FullPivLU.py index bb0ed52b5..f99f3ec61 100644 --- a/unittest/python/test_FullPivLU.py +++ b/unittest/python/test_FullPivLU.py @@ -47,5 +47,4 @@ Q = fullpivlu.permutationQ() reconstructed_matrix = fullpivlu.reconstructedMatrix() - assert eigenpy.is_approx(reconstructed_matrix, A) diff --git a/unittest/python/test_PartialPivLU.py b/unittest/python/test_PartialPivLU.py index 1d0cbbeb0..d76f1f039 100644 --- a/unittest/python/test_PartialPivLU.py +++ b/unittest/python/test_PartialPivLU.py @@ -25,5 +25,7 @@ rcond = partialpivlu.rcond() matrixLU = partialpivlu.matrixLU() -permutationP = partialpivlu.permutationP() +P = partialpivlu.permutationP() + reconstructed_matrix = partialpivlu.reconstructedMatrix() +assert eigenpy.is_approx(reconstructed_matrix, A) From 493cd135f61917e94f2622fec03432f8c8d63c25 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 7 Jul 2025 14:25:50 +0200 Subject: [PATCH 08/43] Added comments and setThreshold default option in SVDBase --- include/eigenpy/decompositions/BDCSVD.hpp | 5 ++++- include/eigenpy/decompositions/JacobiSVD.hpp | 2 ++ include/eigenpy/decompositions/PartialPivLU.hpp | 3 +++ include/eigenpy/decompositions/SVDBase.hpp | 15 +++++++++------ unittest/python/test_BDCSVD.py | 11 +++++++++++ 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/include/eigenpy/decompositions/BDCSVD.hpp b/include/eigenpy/decompositions/BDCSVD.hpp index d3ffb1ec5..bb456af3e 100644 --- a/include/eigenpy/decompositions/BDCSVD.hpp +++ b/include/eigenpy/decompositions/BDCSVD.hpp @@ -25,6 +25,8 @@ struct BDCSVDVisitor template void visit(PyClass &cl) const { cl.def(bp::init<>(bp::arg("self"), "Default constructor")) + // TODO: Management of _Options, put default options for default + // constructor .def(bp::init( bp::args("self", "rows", "cols"), "Default Constructor with memory preallocation. ")) @@ -65,7 +67,8 @@ struct BDCSVDVisitor "specified by the computationOptions parameter. ", bp::return_self<>()) .def("rows", &Solver::rows, bp::arg("self"), - "Returns the number of rows. . ") + "Returns the number of rows. ") + .def("setSwitchSize", &Solver::setSwitchSize, bp::args("self", "s")) .def(SVDBaseVisitor()); } diff --git a/include/eigenpy/decompositions/JacobiSVD.hpp b/include/eigenpy/decompositions/JacobiSVD.hpp index 5381d4164..c187fc7e2 100644 --- a/include/eigenpy/decompositions/JacobiSVD.hpp +++ b/include/eigenpy/decompositions/JacobiSVD.hpp @@ -25,6 +25,8 @@ struct JacobiSVDVisitor template void visit(PyClass &cl) const { cl.def(bp::init<>(bp::arg("self"), "Default constructor")) + // TODO: Management of _Options, put default options for default + // constructor .def(bp::init( bp::args("self", "rows", "cols"), "Default Constructor with memory preallocation. ")) diff --git a/include/eigenpy/decompositions/PartialPivLU.hpp b/include/eigenpy/decompositions/PartialPivLU.hpp index d0460e08b..66649c2db 100644 --- a/include/eigenpy/decompositions/PartialPivLU.hpp +++ b/include/eigenpy/decompositions/PartialPivLU.hpp @@ -53,9 +53,12 @@ struct PartialPivLUSolverVisitor : public boost::python::def_visitor< .def("matrixLU", &Solver::matrixLU, bp::arg("self"), "Returns the LU decomposition matrix.", bp::return_internal_reference<>()) + + // TODO: Expose so that the return type are convertible to np arrays .def("permutationP", &Solver::permutationP, bp::arg("self"), "Returns the permutation P.", bp::return_value_policy()) + #if EIGEN_VERSION_AT_LEAST(3, 3, 0) .def("rcond", &Solver::rcond, bp::arg("self"), "Returns an estimate of the reciprocal condition number of the " diff --git a/include/eigenpy/decompositions/SVDBase.hpp b/include/eigenpy/decompositions/SVDBase.hpp index 8c20e87b8..11c3e8d22 100644 --- a/include/eigenpy/decompositions/SVDBase.hpp +++ b/include/eigenpy/decompositions/SVDBase.hpp @@ -67,12 +67,15 @@ struct SVDBaseVisitor "threshold(). " "The default is NumTraits::epsilon()", bp::return_self<>()) - .def("setThreshold", - (Solver & (Solver::*)(Eigen::Default_t)) & Solver::setThreshold, - bp::args("self", "threshold"), - "Allows to come back to the default behavior, letting Eigen use " - " its default formula for determining the threshold.", - bp::return_self<>()) + .def( + "setThreshold", + +[](Solver &self) -> Solver & { + return self.setThreshold(Eigen::Default); + }, + bp::arg("self"), + "Allows to come back to the default behavior, letting Eigen use " + "its default formula for determining the threshold.", + bp::return_self<>()) .def("singularValues", &Solver::singularValues, bp::arg("self"), "Returns the vector of singular values.", diff --git a/unittest/python/test_BDCSVD.py b/unittest/python/test_BDCSVD.py index 472f23a95..a2c5cb864 100644 --- a/unittest/python/test_BDCSVD.py +++ b/unittest/python/test_BDCSVD.py @@ -18,6 +18,8 @@ assert eigenpy.is_approx(A.dot(X_est), B) # Others +assert bdcsvd.info() == eigenpy.ComputationInfo.Success + cols = bdcsvd.cols() rows = bdcsvd.rows() @@ -30,6 +32,15 @@ nonzerosingval = bdcsvd.nonzeroSingularValues() singularvalues = bdcsvd.singularValues() +S = np.diag(singularvalues) +V_adj = V.conj().T +assert eigenpy.is_approx(A, U @ S @ V_adj) + bdcsvd.setThreshold(1e-8) threshold = bdcsvd.threshold() + +bdcsvd.setThreshold() +threshold = bdcsvd.threshold() + rank = bdcsvd.rank() +bdcsvd.setSwitchSize(10) From d17a0128ef2912aec6b47e7065317a03bfa0b210 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 7 Jul 2025 14:38:21 +0200 Subject: [PATCH 09/43] solvers: Completed tests --- .../decompositions/GeneralizedEigenSolver.hpp | 13 +++++++---- unittest/python/test_complex_eigen_solver.py | 5 ++++ unittest/python/test_complex_schur.py | 7 ++++-- .../python/test_generalized_eigen_solver.py | 23 +++++++++++-------- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp b/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp index 6c14bfc14..43f4c6fb0 100644 --- a/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp +++ b/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp @@ -32,10 +32,16 @@ struct GeneralizedEigenSolverVisitor "Computes the generalized eigendecomposition of given matrix " "pair. ")) + .def("eigenvectors", &Solver::eigenvectors, bp::arg("self"), + "Returns an expression of the computed generalized eigenvectors. ") + // TODO: Expose so that the return type are convertible to np arrays + .def("eigenvalues", &Solver::eigenvalues, bp::arg("self"), + "Returns an expression of the computed generalized eigenvalues. ") + .def("alphas", &Solver::alphas, bp::arg("self"), - "Returns the vectors containing the alpha values ") + "Returns the vectors containing the alpha values. ") .def("betas", &Solver::betas, bp::arg("self"), - "Returns the vectors containing the beta values ") + "Returns the vectors containing the beta values. ") .def("compute", &GeneralizedEigenSolverVisitor::compute_proxy, @@ -50,9 +56,6 @@ struct GeneralizedEigenSolverVisitor "Computes generalized eigendecomposition of given matrix. .", bp::return_self<>()) - .def("eigenvectors", &Solver::eigenvectors, bp::arg("self"), - "Returns an expression of the computed generalized eigenvectors. ") - .def("info", &Solver::info, bp::arg("self"), "NumericalIssue if the input contains INF or NaN values or " "overflow occured. Returns Success otherwise.") diff --git a/unittest/python/test_complex_eigen_solver.py b/unittest/python/test_complex_eigen_solver.py index 4790e0e52..fdf809dba 100644 --- a/unittest/python/test_complex_eigen_solver.py +++ b/unittest/python/test_complex_eigen_solver.py @@ -8,8 +8,13 @@ ces = eigenpy.ComplexEigenSolver(A) +assert ces.info() == eigenpy.ComputationInfo.Success + V = ces.eigenvectors() D = ces.eigenvalues() assert eigenpy.is_approx(A.dot(V).real, V.dot(np.diag(D)).real) assert eigenpy.is_approx(A.dot(V).imag, V.dot(np.diag(D)).imag) + +ces.setMaxIterations(10) +assert ces.getMaxIterations() == 10 diff --git a/unittest/python/test_complex_schur.py b/unittest/python/test_complex_schur.py index 1a9cd2e4c..3afb95b11 100644 --- a/unittest/python/test_complex_schur.py +++ b/unittest/python/test_complex_schur.py @@ -2,7 +2,7 @@ import eigenpy -dim = 100 +dim = 5 rng = np.random.default_rng() A = rng.random((dim, dim)) @@ -13,4 +13,7 @@ U_star = U.conj().T assert eigenpy.is_approx(A.real, (U @ T @ U_star).real) -assert np.allclose(A.imag, (U @ T @ U_star).imag, atol=1e-10) +assert np.allclose(A.imag, (U @ T @ U_star).imag) + +cs.setMaxIterations(10) +assert cs.getMaxIterations() == 10 diff --git a/unittest/python/test_generalized_eigen_solver.py b/unittest/python/test_generalized_eigen_solver.py index 6fcc3ca0d..21dcd5755 100644 --- a/unittest/python/test_generalized_eigen_solver.py +++ b/unittest/python/test_generalized_eigen_solver.py @@ -6,21 +6,24 @@ rng = np.random.default_rng() A = rng.random((dim, dim)) B = rng.random((dim, dim)) +B = (B + B.T) * 0.5 + np.diag(10.0 + rng.random(dim)) # Make B not singular ges = eigenpy.GeneralizedEigenSolver(A, B) +assert ges.info() == eigenpy.ComputationInfo.Success + alphas = ges.alphas() betas = ges.betas() -V = ges.eigenvectors() -eigenvalues = alphas / betas -if np.all(np.abs(betas) >= 1e-15): - for i in range(dim): - v = V[:, i] - lam = eigenvalues[i] +vec = ges.eigenvectors() + +val_est = alphas / betas +for i in range(dim): + v = vec[:, i] + lam = val_est[i] - Av = A @ v - lam_Bv = lam * (B @ v) + Av = A @ v + lam_Bv = lam * (B @ v) - assert eigenpy.is_approx(Av.real, lam_Bv.real) - assert eigenpy.is_approx(Av.imag, lam_Bv.imag) + assert eigenpy.is_approx(Av.real, lam_Bv.real) + assert eigenpy.is_approx(Av.imag, lam_Bv.imag) From 76269d9193e5078cc6a874d2632a0bd2a498cfb7 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 7 Jul 2025 15:30:13 +0200 Subject: [PATCH 10/43] decompositions: comments and rewrites --- .../eigenpy/decompositions/ComplexSchur.hpp | 14 +++--- .../HessenbergDecomposition.hpp | 13 +---- include/eigenpy/decompositions/RealSchur.hpp | 4 +- .../decompositions/Tridiagonalization.hpp | 26 +++------- include/eigenpy/decompositions/sparse/LU.hpp | 48 +++++++++++-------- unittest/python/test_tridiagonalization.py | 11 ----- 6 files changed, 47 insertions(+), 69 deletions(-) diff --git a/include/eigenpy/decompositions/ComplexSchur.hpp b/include/eigenpy/decompositions/ComplexSchur.hpp index fb5586515..0199823a0 100644 --- a/include/eigenpy/decompositions/ComplexSchur.hpp +++ b/include/eigenpy/decompositions/ComplexSchur.hpp @@ -27,6 +27,13 @@ struct ComplexSchurVisitor .def(bp::init>( bp::args("matrix", "computeU"), "Computes Schur of given matrix")) + .def("matrixU", &Solver::matrixU, bp::arg("self"), + "Returns the unitary matrix in the Schur decomposition. ", + bp::return_value_policy()) + .def("matrixT", &Solver::matrixT, bp::arg("self"), + "Returns the triangular matrix in the Schur decomposition. ", + bp::return_value_policy()) + .def("compute", &ComplexSchurVisitor::compute_proxy, bp::args("self", "matrix"), "Computes the Schur of given matrix.", bp::return_self<>()) @@ -46,13 +53,6 @@ struct ComplexSchurVisitor "Compute Schur decomposition from a given Hessenberg matrix. ", bp::return_self<>()) - .def("matrixT", &Solver::matrixT, bp::arg("self"), - "Returns the triangular matrix in the Schur decomposition. ", - bp::return_value_policy()) - .def("matrixU", &Solver::matrixU, bp::arg("self"), - "Returns the unitary matrix in the Schur decomposition. ", - bp::return_value_policy()) - .def("info", &Solver::info, bp::arg("self"), "NumericalIssue if the input contains INF or NaN values or " "overflow occured. Returns Success otherwise.") diff --git a/include/eigenpy/decompositions/HessenbergDecomposition.hpp b/include/eigenpy/decompositions/HessenbergDecomposition.hpp index c1c6e3b66..4a093e25f 100644 --- a/include/eigenpy/decompositions/HessenbergDecomposition.hpp +++ b/include/eigenpy/decompositions/HessenbergDecomposition.hpp @@ -32,11 +32,6 @@ struct HessenbergDecompositionVisitor bp::arg("matrix"), "Constructor; computes Hessenberg decomposition of given matrix. ")) - .def("compute", - &HessenbergDecompositionVisitor::compute_proxy, - bp::args("self", "A"), - "Computes Hessenberg decomposition of given matrix. ", - bp::return_self<>()) .def( "compute", (Solver & (Solver::*)(const Eigen::EigenBase& matrix)) & @@ -49,6 +44,7 @@ struct HessenbergDecompositionVisitor bp::arg("self"), "Returns the Householder coefficients. ", bp::return_value_policy()) + // TODO: Expose so that the return type are convertible to np arrays // matrixH // matrixQ @@ -68,13 +64,6 @@ struct HessenbergDecompositionVisitor .def(HessenbergDecompositionVisitor()) .def(IdVisitor()); } - - private: - template - static Solver& compute_proxy(Solver& self, - const Eigen::EigenBase& matrix) { - return self.compute(matrix); - } }; } // namespace eigenpy diff --git a/include/eigenpy/decompositions/RealSchur.hpp b/include/eigenpy/decompositions/RealSchur.hpp index 24dcf0cb9..7c0be358d 100644 --- a/include/eigenpy/decompositions/RealSchur.hpp +++ b/include/eigenpy/decompositions/RealSchur.hpp @@ -63,7 +63,9 @@ struct RealSchurVisitor .def("setMaxIterations", &Solver::setMaxIterations, bp::args("self", "max_iter"), "Sets the maximum number of iterations allowed.", - bp::return_self<>()); + bp::return_self<>()) + .def("getMaxIterations", &Solver::getMaxIterations, bp::arg("self"), + "Returns the maximum number of iterations."); } static void expose() { diff --git a/include/eigenpy/decompositions/Tridiagonalization.hpp b/include/eigenpy/decompositions/Tridiagonalization.hpp index 4566b3172..9cf86c980 100644 --- a/include/eigenpy/decompositions/Tridiagonalization.hpp +++ b/include/eigenpy/decompositions/Tridiagonalization.hpp @@ -29,10 +29,6 @@ struct TridiagonalizationVisitor : public boost::python::def_visitor< "Constructor; computes tridiagonal " "decomposition of given matrix. ")) - .def("compute", &TridiagonalizationVisitor::compute_proxy, - bp::args("self", "matrix"), - "Computes tridiagonal decomposition of given matrix. ", - bp::return_self<>()) .def( "compute", (Solver & (Solver::*)(const Eigen::EigenBase& matrix)) & @@ -41,22 +37,21 @@ struct TridiagonalizationVisitor : public boost::python::def_visitor< "Computes tridiagonal decomposition of given matrix. ", bp::return_self<>()) - .def("diagonal", &Solver::diagonal, bp::arg("self"), - "Returns the diagonal of the tridiagonal matrix T in the " - "decomposition. ") - .def("householderCoefficients", &Solver::householderCoefficients, bp::arg("self"), "Returns the Householder coefficients. ") + .def("packedMatrix", &Solver::packedMatrix, bp::arg("self"), + "Returns the internal representation of the decomposition. ", + bp::return_value_policy()) + // TODO: Expose so that the return type are convertible to np arrays .def("matrixQ", &Solver::matrixQ, bp::arg("self"), "Returns the unitary matrix Q in the decomposition. ") .def("matrixT", &Solver::matrixT, bp::arg("self"), "Returns the unitary matrix T in the decomposition. ") - .def("packedMatrix", &Solver::packedMatrix, bp::arg("self"), - "Returns the internal representation of the decomposition. ", - bp::return_value_policy()) - + .def("diagonal", &Solver::diagonal, bp::arg("self"), + "Returns the diagonal of the tridiagonal matrix T in the " + "decomposition. ") .def("subDiagonal", &Solver::subDiagonal, bp::arg("self"), "Returns the subdiagonal of the tridiagonal matrix T in the " "decomposition."); @@ -73,13 +68,6 @@ struct TridiagonalizationVisitor : public boost::python::def_visitor< .def(TridiagonalizationVisitor()) .def(IdVisitor()); } - - private: - template - static Solver& compute_proxy(Solver& self, - const Eigen::EigenBase& matrix) { - return self.compute(matrix); - } }; } // namespace eigenpy diff --git a/include/eigenpy/decompositions/sparse/LU.hpp b/include/eigenpy/decompositions/sparse/LU.hpp index c25e49586..a41f353af 100644 --- a/include/eigenpy/decompositions/sparse/LU.hpp +++ b/include/eigenpy/decompositions/sparse/LU.hpp @@ -37,49 +37,59 @@ struct SparseLUVisitor : public boost::python::def_visitor< .def(bp::init(bp::args("self", "matrix"), "Constructs and performs the LU " "factorization from a given matrix.")) + + .def("determinant", &Solver::determinant, bp::arg("self"), + "Returns the determinant of the matrix.") + .def("signDeterminant", &Solver::signDeterminant, bp::arg("self"), + "A number representing the sign of the determinant. ") .def("absDeterminant", &Solver::absDeterminant, bp::arg("self"), "Returns the absolute value of the determinant of the matrix of " "which *this is the QR decomposition.") - .def("analyzePattern", &Solver::analyzePattern, bp::args("self", "mat"), - "Compute the column permutation to minimize the fill-in.") + .def("logAbsDeterminant", &Solver::logAbsDeterminant, bp::arg("self"), + "Returns the natural log of the absolute value of the determinant " + "of the matrix of which *this is the QR decomposition") + .def("colsPermutation", &Solver::colsPermutation, bp::arg("self"), "Returns a reference to the column matrix permutation PTc such " "that Pr A PTc = LU.", bp::return_value_policy()) + .def("rowsPermutation", &Solver::rowsPermutation, bp::arg("self"), + "Returns a reference to the row matrix permutation Pr such that " + "Pr A PTc = LU", + bp::return_value_policy()) + .def("compute", &Solver::compute, bp::args("self", "matrix"), "Compute the symbolic and numeric factorization of the input " "sparse matrix. " "The input matrix should be in column-major storage. ") - .def("determinant", &Solver::determinant, bp::arg("self"), - "Returns the determinant of the matrix.") + + .def("analyzePattern", &Solver::analyzePattern, + bp::args("self", "matrix"), + "Compute the column permutation to minimize the fill-in.") .def("factorize", &Solver::factorize, bp::args("self", "matrix"), "Performs a numeric decomposition of a given matrix.\n" "The given matrix must has the same sparcity than the matrix on " "which the symbolic decomposition has been performed.") - .def("info", &Solver::info, bp::arg("self"), - "NumericalIssue if the input contains INF or NaN values or " - "overflow occured. Returns Success otherwise.") + // TODO: Expose so that the return type are convertible to np arrays + // transpose() + // adjoint() + // matrixL() + // matrixU() .def("isSymmetric", &Solver::isSymmetric, bp::args("self", "sym"), "Indicate that the pattern of the input matrix is symmetric. ") - .def("lastErrorMessage", &Solver::lastErrorMessage, bp::arg("self"), - "Returns a string describing the type of error. ") - .def("logAbsDeterminant", &Solver::logAbsDeterminant, bp::arg("self"), - "Returns the natural log of the absolute value of the determinant " - "of the " - "matrix of which *this is the QR decomposition") - .def("rowsPermutation", &Solver::rowsPermutation, bp::arg("self"), - "Returns a reference to the row matrix permutation Pr such that " - "Pr A PTc = LU", - bp::return_value_policy()) .def("setPivotThreshold", &Solver::setPivotThreshold, bp::args("self", "thresh"), "Set the threshold used for a diagonal entry to be an acceptable " "pivot.") - .def("signDeterminant", &Solver::signDeterminant, bp::arg("self"), - "A number representing the sign of the determinant. ") + + .def("info", &Solver::info, bp::arg("self"), + "NumericalIssue if the input contains INF or NaN values or " + "overflow occured. Returns Success otherwise.") + .def("lastErrorMessage", &Solver::lastErrorMessage, bp::arg("self"), + "Returns a string describing the type of error. ") .def(SparseSolverBaseVisitor()); } diff --git a/unittest/python/test_tridiagonalization.py b/unittest/python/test_tridiagonalization.py index bdf86aa7f..894301b76 100644 --- a/unittest/python/test_tridiagonalization.py +++ b/unittest/python/test_tridiagonalization.py @@ -7,14 +7,3 @@ A = rng.random((dim, dim)) tri = eigenpy.Tridiagonalization(A) - -# Q = tri.matrixQ() -# print("Q") -# print(Q) -# Q_conj = Q.conj().T - -# T = tri.matrixT() -# print("T") -# print(T) - -# assert eigenpy.is_approx(A, Q @ T @ Q_conj) From 8aa33b96da8ac1468cafd0a70a259473ba4d2353 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 7 Jul 2025 16:23:08 +0200 Subject: [PATCH 11/43] decompositions: Put ordering option AMD in SparseQR --- include/eigenpy/decompositions/sparse/QR.hpp | 30 +++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/include/eigenpy/decompositions/sparse/QR.hpp b/include/eigenpy/decompositions/sparse/QR.hpp index 7ea902ad0..182662382 100644 --- a/include/eigenpy/decompositions/sparse/QR.hpp +++ b/include/eigenpy/decompositions/sparse/QR.hpp @@ -15,8 +15,8 @@ namespace eigenpy { template > // TODO: Check ordering + typename _Ordering = Eigen::AMDOrdering< + typename _MatrixType::StorageIndex>> struct SparseQRVisitor : public boost::python::def_visitor< SparseQRVisitor<_MatrixType, _Ordering>> { typedef SparseQRVisitor<_MatrixType, _Ordering> Visitor; @@ -40,33 +40,43 @@ struct SparseQRVisitor : public boost::python::def_visitor< .def(bp::init( bp::args("self", "mat"), "Construct a QR factorization of the matrix mat.")) - .def("analyzePattern", &Solver::analyzePattern, bp::args("self", "mat"), - "Compute the column permutation to minimize the fill-in.") + .def("cols", &Solver::cols, bp::arg("self"), "Returns the number of columns of the represented matrix. ") - .def("colsPermutation", &Solver::colsPermutation, bp::arg("self"), - "Returns a reference to the column matrix permutation PTc such " - "that Pr A PTc = LU.", - bp::return_value_policy()) + .def("rows", &Solver::rows, bp::arg("self"), + "Returns the number of rows of the represented matrix. ") + .def("compute", &Solver::compute, bp::args("self", "matrix"), "Compute the symbolic and numeric factorization of the input " "sparse matrix. " "The input matrix should be in column-major storage. ") + .def("analyzePattern", &Solver::analyzePattern, bp::args("self", "mat"), + "Compute the column permutation to minimize the fill-in.") .def("factorize", &Solver::factorize, bp::args("self", "matrix"), "Performs a numeric decomposition of a given matrix.\n" "The given matrix must has the same sparcity than the matrix on " "which the symbolic decomposition has been performed.") + + // TODO: Expose so that the return type are convertible to np arrays + // matrixQ + // matrixR + + .def("colsPermutation", &Solver::colsPermutation, bp::arg("self"), + "Returns a reference to the column matrix permutation PTc such " + "that Pr A PTc = LU.", + bp::return_value_policy()) + .def("info", &Solver::info, bp::arg("self"), "NumericalIssue if the input contains INF or NaN values or " "overflow occured. Returns Success otherwise.") .def("lastErrorMessage", &Solver::lastErrorMessage, bp::arg("self"), "Returns a string describing the type of error. ") + .def("rank", &Solver::rank, bp::arg("self"), "Returns the number of non linearly dependent columns as " "determined " "by the pivoting threshold. ") - .def("rows", &Solver::rows, bp::arg("self"), - "Returns the number of rows of the represented matrix. ") + .def("setPivotThreshold", &Solver::setPivotThreshold, bp::args("self", "thresh"), "Set the threshold used for a diagonal entry to be an acceptable " From 0ba3768b695644a635a839dd7df786ac2240448d Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 7 Jul 2025 17:23:07 +0200 Subject: [PATCH 12/43] decompositions: Fixed computation options in BDCSVD --- include/eigenpy/decompositions/BDCSVD.hpp | 18 ++++-------------- include/eigenpy/decompositions/sparse/QR.hpp | 4 ++-- unittest/python/test_BDCSVD.py | 9 ++++++--- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/include/eigenpy/decompositions/BDCSVD.hpp b/include/eigenpy/decompositions/BDCSVD.hpp index bb456af3e..78d5f2680 100644 --- a/include/eigenpy/decompositions/BDCSVD.hpp +++ b/include/eigenpy/decompositions/BDCSVD.hpp @@ -25,28 +25,18 @@ struct BDCSVDVisitor template void visit(PyClass &cl) const { cl.def(bp::init<>(bp::arg("self"), "Default constructor")) - // TODO: Management of _Options, put default options for default - // constructor .def(bp::init( bp::args("self", "rows", "cols"), "Default Constructor with memory preallocation. ")) .def(bp::init( - bp::args("self", "rows", "cols", "computationOptions "), - "Default Constructor with memory preallocation. \n\n" - "Like the default constructor but with preallocation of the " - "internal " - "data according to the specified problem size and the " - "computationOptions. ")) + bp::args("self", "rows", "cols", "computationOptions"), + "Default Constructor with memory preallocation. ")) .def(bp::init( bp::args("self", "matrix"), "Constructor performing the decomposition of given matrix. ")) .def(bp::init( - bp::args("self", "matrix", "computationOptions "), - "Constructor performing the decomposition of given matrix. \n\n" - "One cannot request unitiaries using both the Options template " - "parameter " - "and the constructor. If possible, prefer using the Options " - "template parameter.")) + bp::args("self", "matrix", "computationOptions"), + "Constructor performing the decomposition of given matrix. ")) .def("cols", &Solver::cols, bp::arg("self"), "Returns the number of columns. ") diff --git a/include/eigenpy/decompositions/sparse/QR.hpp b/include/eigenpy/decompositions/sparse/QR.hpp index 182662382..105748913 100644 --- a/include/eigenpy/decompositions/sparse/QR.hpp +++ b/include/eigenpy/decompositions/sparse/QR.hpp @@ -15,8 +15,8 @@ namespace eigenpy { template > + typename _Ordering = + Eigen::AMDOrdering> struct SparseQRVisitor : public boost::python::def_visitor< SparseQRVisitor<_MatrixType, _Ordering>> { typedef SparseQRVisitor<_MatrixType, _Ordering> Visitor; diff --git a/unittest/python/test_BDCSVD.py b/unittest/python/test_BDCSVD.py index a2c5cb864..bb78da306 100644 --- a/unittest/python/test_BDCSVD.py +++ b/unittest/python/test_BDCSVD.py @@ -8,7 +8,12 @@ A = rng.random((dim, dim)) A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -bdcsvd = eigenpy.BDCSVD(A, 24) +bdcsvd = eigenpy.BDCSVD( + A, + eigenpy.DecompositionOptions.ComputeFullU + | eigenpy.DecompositionOptions.ComputeFullV, +) +assert bdcsvd.info() == eigenpy.ComputationInfo.Success # Solve X = rng.random((dim, 20)) @@ -18,8 +23,6 @@ assert eigenpy.is_approx(A.dot(X_est), B) # Others -assert bdcsvd.info() == eigenpy.ComputationInfo.Success - cols = bdcsvd.cols() rows = bdcsvd.rows() From b2ccf7dd7db750fc1d229c1f88cdb99315c79bb2 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 7 Jul 2025 17:26:57 +0200 Subject: [PATCH 13/43] decompositions: Rewrite comments in constructors in BDCSVD and JacobiSVD --- include/eigenpy/decompositions/BDCSVD.hpp | 18 +++++++++++++----- include/eigenpy/decompositions/JacobiSVD.hpp | 4 +--- unittest/python/test_BDCSVD.py | 9 ++++----- unittest/python/test_JacobiSVD.py | 14 +++++++++++++- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/include/eigenpy/decompositions/BDCSVD.hpp b/include/eigenpy/decompositions/BDCSVD.hpp index 78d5f2680..2b15faf0e 100644 --- a/include/eigenpy/decompositions/BDCSVD.hpp +++ b/include/eigenpy/decompositions/BDCSVD.hpp @@ -29,14 +29,22 @@ struct BDCSVDVisitor bp::args("self", "rows", "cols"), "Default Constructor with memory preallocation. ")) .def(bp::init( - bp::args("self", "rows", "cols", "computationOptions"), - "Default Constructor with memory preallocation. ")) + bp::args("self", "rows", "cols", "computationOptions "), + "Default Constructor with memory preallocation. \n\n" + "Like the default constructor but with preallocation of the " + "internal " + "data according to the specified problem size and the " + "computationOptions. ")) .def(bp::init( bp::args("self", "matrix"), "Constructor performing the decomposition of given matrix. ")) .def(bp::init( - bp::args("self", "matrix", "computationOptions"), - "Constructor performing the decomposition of given matrix. ")) + bp::args("self", "matrix", "computationOptions "), + "Constructor performing the decomposition of given matrix. \n\n" + "One cannot request unitiaries using both the Options template " + "parameter " + "and the constructor. If possible, prefer using the Options " + "template parameter.")) .def("cols", &Solver::cols, bp::arg("self"), "Returns the number of columns. ") @@ -52,7 +60,7 @@ struct BDCSVDVisitor (Solver & (Solver::*)(const MatrixType &matrix, unsigned int computationOptions)) & Solver::compute, - bp::args("self", "matrix"), + bp::args("self", "matrix", "computationOptions"), "Method performing the decomposition of given matrix, as " "specified by the computationOptions parameter. ", bp::return_self<>()) diff --git a/include/eigenpy/decompositions/JacobiSVD.hpp b/include/eigenpy/decompositions/JacobiSVD.hpp index c187fc7e2..75b3edd9a 100644 --- a/include/eigenpy/decompositions/JacobiSVD.hpp +++ b/include/eigenpy/decompositions/JacobiSVD.hpp @@ -25,8 +25,6 @@ struct JacobiSVDVisitor template void visit(PyClass &cl) const { cl.def(bp::init<>(bp::arg("self"), "Default constructor")) - // TODO: Management of _Options, put default options for default - // constructor .def(bp::init( bp::args("self", "rows", "cols"), "Default Constructor with memory preallocation. ")) @@ -62,7 +60,7 @@ struct JacobiSVDVisitor (Solver & (Solver::*)(const MatrixType &matrix, unsigned int computationOptions)) & Solver::compute, - bp::args("self", "matrix"), + bp::args("self", "matrix", "computationOptions"), "Method performing the decomposition of given matrix, as " "specified by the computationOptions parameter. ", bp::return_self<>()) diff --git a/unittest/python/test_BDCSVD.py b/unittest/python/test_BDCSVD.py index bb78da306..1033c8ca3 100644 --- a/unittest/python/test_BDCSVD.py +++ b/unittest/python/test_BDCSVD.py @@ -8,11 +8,10 @@ A = rng.random((dim, dim)) A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -bdcsvd = eigenpy.BDCSVD( - A, - eigenpy.DecompositionOptions.ComputeFullU - | eigenpy.DecompositionOptions.ComputeFullV, -) +opt_U = eigenpy.DecompositionOptions.ComputeFullU +opt_V = eigenpy.DecompositionOptions.ComputeFullV + +bdcsvd = eigenpy.BDCSVD(A, opt_U | opt_V) assert bdcsvd.info() == eigenpy.ComputationInfo.Success # Solve diff --git a/unittest/python/test_JacobiSVD.py b/unittest/python/test_JacobiSVD.py index 707ba6e81..0559c89db 100644 --- a/unittest/python/test_JacobiSVD.py +++ b/unittest/python/test_JacobiSVD.py @@ -8,7 +8,11 @@ A = rng.random((dim, dim)) A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -jacobisvd = eigenpy.JacobiSVD(A, 24) +opt_U = eigenpy.DecompositionOptions.ComputeFullU +opt_V = eigenpy.DecompositionOptions.ComputeFullV + +jacobisvd = eigenpy.JacobiSVD(A, opt_U | opt_V) +assert jacobisvd.info() == eigenpy.ComputationInfo.Success # Solve X = rng.random((dim, 20)) @@ -30,6 +34,14 @@ nonzerosingval = jacobisvd.nonzeroSingularValues() singularvalues = jacobisvd.singularValues() +S = np.diag(singularvalues) +V_adj = V.conj().T +assert eigenpy.is_approx(A, U @ S @ V_adj) + jacobisvd.setThreshold(1e-8) threshold = jacobisvd.threshold() + +jacobisvd.setThreshold() +threshold = jacobisvd.threshold() + rank = jacobisvd.rank() From e3a256e8967c915aa5836f029ccef614aed9e3a5 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Sun, 20 Jul 2025 04:04:47 +0200 Subject: [PATCH 14/43] Set COLAMD Ordering in SParseQR --- include/eigenpy/decompositions/sparse/QR.hpp | 2 +- unittest/python/test_complex_schur.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/include/eigenpy/decompositions/sparse/QR.hpp b/include/eigenpy/decompositions/sparse/QR.hpp index 105748913..ef95109a9 100644 --- a/include/eigenpy/decompositions/sparse/QR.hpp +++ b/include/eigenpy/decompositions/sparse/QR.hpp @@ -16,7 +16,7 @@ namespace eigenpy { template > + Eigen::COLAMDOrdering> struct SparseQRVisitor : public boost::python::def_visitor< SparseQRVisitor<_MatrixType, _Ordering>> { typedef SparseQRVisitor<_MatrixType, _Ordering> Visitor; diff --git a/unittest/python/test_complex_schur.py b/unittest/python/test_complex_schur.py index 3afb95b11..de5eaa1ad 100644 --- a/unittest/python/test_complex_schur.py +++ b/unittest/python/test_complex_schur.py @@ -13,7 +13,6 @@ U_star = U.conj().T assert eigenpy.is_approx(A.real, (U @ T @ U_star).real) -assert np.allclose(A.imag, (U @ T @ U_star).imag) cs.setMaxIterations(10) assert cs.getMaxIterations() == 10 From 8cbaf196e92e7d1307b94dda057eeb3db83240ed Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Sun, 20 Jul 2025 16:31:47 +0200 Subject: [PATCH 15/43] decompositions: Completed tests --- include/eigenpy/decompositions/FullPivLU.hpp | 10 +- .../decompositions/GeneralizedEigenSolver.hpp | 7 +- .../HessenbergDecomposition.hpp | 11 +- include/eigenpy/decompositions/JacobiSVD.hpp | 26 +-- .../eigenpy/decompositions/PartialPivLU.hpp | 10 +- .../decompositions/Tridiagonalization.hpp | 39 ++-- include/eigenpy/decompositions/sparse/LU.hpp | 16 +- include/eigenpy/decompositions/sparse/QR.hpp | 4 - src/decompositions/jacobisvd-solver.cpp | 19 +- .../decompositions/sparse/test_SparseLU.py | 44 ++--- .../decompositions/sparse/test_SparseQR.py | 42 ++--- unittest/python/test_BDCSVD.py | 154 ++++++++++++---- unittest/python/test_FullPivLU.py | 107 ++++++++--- unittest/python/test_JacobiSVD.py | 167 ++++++++++++++---- unittest/python/test_PartialPivLU.py | 65 +++++-- unittest/python/test_complex_eigen_solver.py | 90 +++++++++- unittest/python/test_complex_schur.py | 110 +++++++++++- .../python/test_generalized_eigen_solver.py | 114 ++++++++++-- ...t_generalized_self_adjoint_eigen_solver.py | 119 +++++++++++-- .../python/test_hessenberg_decomposition.py | 64 ++++++- unittest/python/test_real_qz.py | 85 ++++++++- unittest/python/test_real_schur.py | 124 ++++++++++++- unittest/python/test_tridiagonalization.py | 95 ++++++++++ 23 files changed, 1271 insertions(+), 251 deletions(-) diff --git a/include/eigenpy/decompositions/FullPivLU.hpp b/include/eigenpy/decompositions/FullPivLU.hpp index a6e8e4a40..0ef2cb3e2 100644 --- a/include/eigenpy/decompositions/FullPivLU.hpp +++ b/include/eigenpy/decompositions/FullPivLU.hpp @@ -33,8 +33,6 @@ struct FullPivLUSolverVisitor .def(bp::init( bp::args("self", "rows", "cols"), "Default constructor with memory preallocation")) - .def(bp::init(bp::args("self", "matrix"), - "Constructor.")) .def(bp::init( bp::args("self", "matrix"), "Constructs a LU factorization from a given matrix.")) @@ -57,7 +55,7 @@ struct FullPivLUSolverVisitor "is the LU decomposition.") .def( "image", - +[](const Solver &self, const MatrixType &mat) -> MatrixType { + +[](Solver &self, const MatrixType &mat) -> MatrixType { return self.image(mat); }, bp::args("self", "originalMatrix"), @@ -66,7 +64,7 @@ struct FullPivLUSolverVisitor "image (column-space).") .def( "inverse", - +[](const Solver &self) -> MatrixType { return self.inverse(); }, + +[](Solver &self) -> MatrixType { return self.inverse(); }, bp::arg("self"), "Returns the inverse of the matrix of which *this is the LU " "decomposition.") @@ -76,8 +74,7 @@ struct FullPivLUSolverVisitor .def("isSurjective", &Solver::isSurjective, bp::arg("self")) .def( - "kernel", - +[](const Solver &self) -> MatrixType { return self.kernel(); }, + "kernel", +[](Solver &self) -> MatrixType { return self.kernel(); }, bp::arg("self"), "Returns the kernel of the matrix, also called its null-space. " "The columns of the returned matrix will form a basis of the " @@ -90,7 +87,6 @@ struct FullPivLUSolverVisitor .def("maxPivot", &Solver::maxPivot, bp::arg("self")) .def("nonzeroPivots", &Solver::nonzeroPivots, bp::arg("self")) - // TODO: Expose so that the return type are convertible to np arrays .def("permutationP", &Solver::permutationP, bp::arg("self"), "Returns the permutation P.", bp::return_value_policy()) diff --git a/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp b/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp index 43f4c6fb0..d2b54820b 100644 --- a/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp +++ b/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp @@ -34,9 +34,10 @@ struct GeneralizedEigenSolverVisitor .def("eigenvectors", &Solver::eigenvectors, bp::arg("self"), "Returns an expression of the computed generalized eigenvectors. ") - // TODO: Expose so that the return type are convertible to np arrays - .def("eigenvalues", &Solver::eigenvalues, bp::arg("self"), - "Returns an expression of the computed generalized eigenvalues. ") + .def( + "eigenvalues", + +[](const Solver& c) { return c.eigenvalues().eval(); }, + "Returns the computed generalized eigenvalues.") .def("alphas", &Solver::alphas, bp::arg("self"), "Returns the vectors containing the alpha values. ") diff --git a/include/eigenpy/decompositions/HessenbergDecomposition.hpp b/include/eigenpy/decompositions/HessenbergDecomposition.hpp index 4a093e25f..e9a58aefb 100644 --- a/include/eigenpy/decompositions/HessenbergDecomposition.hpp +++ b/include/eigenpy/decompositions/HessenbergDecomposition.hpp @@ -44,9 +44,14 @@ struct HessenbergDecompositionVisitor bp::arg("self"), "Returns the Householder coefficients. ", bp::return_value_policy()) - // TODO: Expose so that the return type are convertible to np arrays - // matrixH - // matrixQ + .def( + "matrixQ", + +[](const Solver& c) -> MatrixType { return c.matrixQ(); }, + "Reconstructs the orthogonal matrix Q in the decomposition.") + .def( + "matrixH", + +[](const Solver& c) -> MatrixType { return c.matrixH(); }, + "Constructs the Hessenberg matrix H in the decomposition.") .def("packedMatrix", &Solver::packedMatrix, bp::arg("self"), "Returns the internal representation of the decomposition. ", diff --git a/include/eigenpy/decompositions/JacobiSVD.hpp b/include/eigenpy/decompositions/JacobiSVD.hpp index 75b3edd9a..b1fd64d7b 100644 --- a/include/eigenpy/decompositions/JacobiSVD.hpp +++ b/include/eigenpy/decompositions/JacobiSVD.hpp @@ -15,11 +15,10 @@ namespace eigenpy { -template +template struct JacobiSVDVisitor - : public boost::python::def_visitor> { - typedef _MatrixType MatrixType; - typedef Eigen::JacobiSVD Solver; + : public boost::python::def_visitor> { + typedef typename JacobiSVD::MatrixType MatrixType; typedef typename MatrixType::Scalar Scalar; template @@ -46,10 +45,11 @@ struct JacobiSVDVisitor "and the constructor. If possible, prefer using the Options " "template parameter.")) - .def("cols", &Solver::cols, bp::arg("self"), + .def("cols", &JacobiSVD::cols, bp::arg("self"), "Returns the number of columns. ") .def("compute", - (Solver & (Solver::*)(const MatrixType &matrix)) & Solver::compute, + (JacobiSVD & (JacobiSVD::*)(const MatrixType &matrix)) & + JacobiSVD::compute, bp::args("self", "matrix"), "Method performing the decomposition of given matrix. Computes " "Thin/Full " @@ -57,17 +57,17 @@ struct JacobiSVDVisitor "or the class constructor. ", bp::return_self<>()) .def("compute", - (Solver & (Solver::*)(const MatrixType &matrix, - unsigned int computationOptions)) & - Solver::compute, + (JacobiSVD & (JacobiSVD::*)(const MatrixType &matrix, + unsigned int computationOptions)) & + JacobiSVD::compute, bp::args("self", "matrix", "computationOptions"), "Method performing the decomposition of given matrix, as " "specified by the computationOptions parameter. ", bp::return_self<>()) - .def("rows", &Solver::rows, bp::arg("self"), + .def("rows", &JacobiSVD::rows, bp::arg("self"), "Returns the number of rows. . ") - .def(SVDBaseVisitor()); + .def(SVDBaseVisitor()); } static void expose() { @@ -77,7 +77,7 @@ struct JacobiSVDVisitor } static void expose(const std::string &name) { - bp::class_( + bp::class_( name.c_str(), "Two-sided Jacobi SVD decomposition of a rectangular matrix. \n\n" "SVD decomposition consists in decomposing any n-by-p matrix A as a " @@ -109,7 +109,7 @@ struct JacobiSVDVisitor "solving.", bp::no_init) .def(JacobiSVDVisitor()) - .def(IdVisitor()); + .def(IdVisitor()); } }; diff --git a/include/eigenpy/decompositions/PartialPivLU.hpp b/include/eigenpy/decompositions/PartialPivLU.hpp index 66649c2db..28bb3ba1b 100644 --- a/include/eigenpy/decompositions/PartialPivLU.hpp +++ b/include/eigenpy/decompositions/PartialPivLU.hpp @@ -33,8 +33,6 @@ struct PartialPivLUSolverVisitor : public boost::python::def_visitor< .def(bp::init( bp::args("self", "size"), "Default constructor with memory preallocation")) - .def(bp::init(bp::args("self", "matrix"), - "Constructor.")) .def(bp::init( bp::args("self", "matrix"), "Constructs a LU factorization from a given matrix.")) @@ -44,6 +42,13 @@ struct PartialPivLUSolverVisitor : public boost::python::def_visitor< .def("determinant", &Solver::determinant, bp::arg("self"), "Returns the determinant of the matrix of which *this is the LU " "decomposition.") + .def( + "compute", + (Solver & (Solver::*)(const Eigen::EigenBase &matrix)) & + Solver::compute, + bp::args("self", "matrix"), + "Computes the LU factorization of given matrix.", + bp::return_self<>()) .def( "inverse", +[](const Solver &self) -> MatrixType { return self.inverse(); }, @@ -54,7 +59,6 @@ struct PartialPivLUSolverVisitor : public boost::python::def_visitor< "Returns the LU decomposition matrix.", bp::return_internal_reference<>()) - // TODO: Expose so that the return type are convertible to np arrays .def("permutationP", &Solver::permutationP, bp::arg("self"), "Returns the permutation P.", bp::return_value_policy()) diff --git a/include/eigenpy/decompositions/Tridiagonalization.hpp b/include/eigenpy/decompositions/Tridiagonalization.hpp index 9cf86c980..d3cd96f56 100644 --- a/include/eigenpy/decompositions/Tridiagonalization.hpp +++ b/include/eigenpy/decompositions/Tridiagonalization.hpp @@ -20,9 +20,10 @@ struct TridiagonalizationVisitor : public boost::python::def_visitor< typedef _MatrixType MatrixType; typedef typename MatrixType::Scalar Scalar; typedef Eigen::Tridiagonalization Solver; + typedef Eigen::VectorXd VectorType; template - void visit(PyClass& cl) const { + void visit(PyClass &cl) const { cl.def( bp::init(bp::arg("size"), "Default constructor. ")) .def(bp::init(bp::arg("matrix"), @@ -31,7 +32,7 @@ struct TridiagonalizationVisitor : public boost::python::def_visitor< .def( "compute", - (Solver & (Solver::*)(const Eigen::EigenBase& matrix)) & + (Solver & (Solver::*)(const Eigen::EigenBase &matrix)) & Solver::compute, bp::args("self", "matrix"), "Computes tridiagonal decomposition of given matrix. ", @@ -43,18 +44,28 @@ struct TridiagonalizationVisitor : public boost::python::def_visitor< "Returns the internal representation of the decomposition. ", bp::return_value_policy()) - // TODO: Expose so that the return type are convertible to np arrays - .def("matrixQ", &Solver::matrixQ, bp::arg("self"), - "Returns the unitary matrix Q in the decomposition. ") - .def("matrixT", &Solver::matrixT, bp::arg("self"), - "Returns the unitary matrix T in the decomposition. ") + .def( + "matrixQ", + +[](const Solver &c) -> MatrixType { return c.matrixQ(); }, + "Returns the unitary matrix Q in the decomposition.") + .def( + "matrixT", +[](Solver &c) -> MatrixType { return c.matrixT(); }, + "Returns an expression of the tridiagonal matrix T in the " + "decomposition.") - .def("diagonal", &Solver::diagonal, bp::arg("self"), - "Returns the diagonal of the tridiagonal matrix T in the " - "decomposition. ") - .def("subDiagonal", &Solver::subDiagonal, bp::arg("self"), - "Returns the subdiagonal of the tridiagonal matrix T in the " - "decomposition."); + .def( + "diagonal", + +[](const Solver &c) -> VectorType { return c.diagonal(); }, + bp::arg("self"), + "Returns the diagonal of the tridiagonal matrix T in the " + "decomposition. ") + + .def( + "subDiagonal", + +[](const Solver &c) -> VectorType { return c.subDiagonal(); }, + bp::arg("self"), + "Returns the subdiagonal of the tridiagonal matrix T in the " + "decomposition."); } static void expose() { @@ -63,7 +74,7 @@ struct TridiagonalizationVisitor : public boost::python::def_visitor< expose(classname); } - static void expose(const std::string& name) { + static void expose(const std::string &name) { bp::class_(name.c_str(), bp::no_init) .def(TridiagonalizationVisitor()) .def(IdVisitor()); diff --git a/include/eigenpy/decompositions/sparse/LU.hpp b/include/eigenpy/decompositions/sparse/LU.hpp index a41f353af..5c1a6a979 100644 --- a/include/eigenpy/decompositions/sparse/LU.hpp +++ b/include/eigenpy/decompositions/sparse/LU.hpp @@ -49,6 +49,16 @@ struct SparseLUVisitor : public boost::python::def_visitor< "Returns the natural log of the absolute value of the determinant " "of the matrix of which *this is the QR decomposition") + .def("rows", &Solver::rows, bp::arg("self"), + "Returns the number of rows of the matrix.") + .def("cols", &Solver::cols, bp::arg("self"), + "Returns the number of cols of the matrix.") + + .def("nnzL", &Solver::nnzL, bp::arg("self"), + "The number of non zero elements in L") + .def("nnzU", &Solver::nnzU, bp::arg("self"), + "The number of non zero elements in L") + .def("colsPermutation", &Solver::colsPermutation, bp::arg("self"), "Returns a reference to the column matrix permutation PTc such " "that Pr A PTc = LU.", @@ -71,12 +81,6 @@ struct SparseLUVisitor : public boost::python::def_visitor< "The given matrix must has the same sparcity than the matrix on " "which the symbolic decomposition has been performed.") - // TODO: Expose so that the return type are convertible to np arrays - // transpose() - // adjoint() - // matrixL() - // matrixU() - .def("isSymmetric", &Solver::isSymmetric, bp::args("self", "sym"), "Indicate that the pattern of the input matrix is symmetric. ") diff --git a/include/eigenpy/decompositions/sparse/QR.hpp b/include/eigenpy/decompositions/sparse/QR.hpp index ef95109a9..ea64117ae 100644 --- a/include/eigenpy/decompositions/sparse/QR.hpp +++ b/include/eigenpy/decompositions/sparse/QR.hpp @@ -57,10 +57,6 @@ struct SparseQRVisitor : public boost::python::def_visitor< "The given matrix must has the same sparcity than the matrix on " "which the symbolic decomposition has been performed.") - // TODO: Expose so that the return type are convertible to np arrays - // matrixQ - // matrixR - .def("colsPermutation", &Solver::colsPermutation, bp::arg("self"), "Returns a reference to the column matrix permutation PTc such " "that Pr A PTc = LU.", diff --git a/src/decompositions/jacobisvd-solver.cpp b/src/decompositions/jacobisvd-solver.cpp index 5b2234861..7abf93a63 100644 --- a/src/decompositions/jacobisvd-solver.cpp +++ b/src/decompositions/jacobisvd-solver.cpp @@ -7,6 +7,23 @@ namespace eigenpy { void exposeJacobiSVDSolver() { using namespace Eigen; - JacobiSVDVisitor::expose("JacobiSVD"); + using Eigen::JacobiSVD; + + using Eigen::ColPivHouseholderQRPreconditioner; + using Eigen::FullPivHouseholderQRPreconditioner; + using Eigen::HouseholderQRPreconditioner; + using Eigen::NoQRPreconditioner; + + using ColPivHhJacobiSVD = + JacobiSVD; + using FullPivHhJacobiSVD = + JacobiSVD; + using HhJacobiSVD = JacobiSVD; + using NoPrecondJacobiSVD = JacobiSVD; + + JacobiSVDVisitor::expose("ColPivHhJacobiSVD"); + JacobiSVDVisitor::expose("FullPivHhJacobiSVD"); + JacobiSVDVisitor::expose("HhJacobiSVD"); + JacobiSVDVisitor::expose("NoPrecondJacobiSVD"); } } // namespace eigenpy diff --git a/unittest/python/decompositions/sparse/test_SparseLU.py b/unittest/python/decompositions/sparse/test_SparseLU.py index 81fd71e50..55f0b3ae2 100644 --- a/unittest/python/decompositions/sparse/test_SparseLU.py +++ b/unittest/python/decompositions/sparse/test_SparseLU.py @@ -1,48 +1,38 @@ import numpy as np -import scipy -from scipy.sparse import csc_matrix +import scipy.sparse as spa import eigenpy dim = 100 rng = np.random.default_rng() -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -A = csc_matrix(A) +A_fac = spa.random(dim, dim, density=0.25, random_state=rng) +A = A_fac.T @ A_fac +A += spa.diags(10.0 * rng.standard_normal(dim) ** 2) +A = A.tocsc(True) +A.check_format() -sparselu = eigenpy.SparseLU(A) +splu = eigenpy.SparseLU(A) -assert sparselu.info() == eigenpy.ComputationInfo.Success +assert splu.info() == eigenpy.ComputationInfo.Success -# Solve X = rng.random((dim, 20)) B = A.dot(X) -X_est = sparselu.solve(B) +X_est = splu.solve(B) +assert isinstance(X_est, np.ndarray) assert eigenpy.is_approx(X, X_est) assert eigenpy.is_approx(A.dot(X_est), B) -X_sparse = scipy.sparse.random(dim, 10) -B_sparse = A.dot(X_sparse) -B_sparse = B_sparse.tocsc(True) +splu.analyzePattern(A) +splu.factorize(A) +X_sparse = spa.random(dim, 10, random_state=rng) +B_sparse = A.dot(X_sparse) +B_sparse: spa.csc_matrix = B_sparse.tocsc(True) if not B_sparse.has_sorted_indices: B_sparse.sort_indices() -X_est = sparselu.solve(B_sparse) +X_est = splu.solve(B_sparse) +assert isinstance(X_est, spa.csc_matrix) assert eigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) assert eigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) - -# Others -det = sparselu.determinant() -sign_det = sparselu.signDeterminant() -abs_det = sparselu.absDeterminant() -log_abs_det = sparselu.logAbsDeterminant() - -sparselu.analyzePattern(A) -sparselu.factorize(A) - -cols_permutation = sparselu.colsPermutation() -rows_permutation = sparselu.rowsPermutation() - -sparselu.setPivotThreshold(1e-8) diff --git a/unittest/python/decompositions/sparse/test_SparseQR.py b/unittest/python/decompositions/sparse/test_SparseQR.py index eda8ff33f..0d37179e3 100644 --- a/unittest/python/decompositions/sparse/test_SparseQR.py +++ b/unittest/python/decompositions/sparse/test_SparseQR.py @@ -1,46 +1,38 @@ import numpy as np -import scipy -from scipy.sparse import csc_matrix +import scipy.sparse as spa import eigenpy dim = 100 rng = np.random.default_rng() -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -A = csc_matrix(A) +A_fac = spa.random(dim, dim, density=0.25, random_state=rng) +A = A_fac.T @ A_fac +A += spa.diags(10.0 * rng.standard_normal(dim) ** 2) +A = A.tocsc(True) +A.check_format() -sparseqr = eigenpy.SparseQR(A) +spqr = eigenpy.SparseQR(A) -assert sparseqr.info() == eigenpy.ComputationInfo.Success +assert spqr.info() == eigenpy.ComputationInfo.Success -# Solve X = rng.random((dim, 20)) B = A.dot(X) -X_est = sparseqr.solve(B) +X_est = spqr.solve(B) +assert isinstance(X_est, np.ndarray) assert eigenpy.is_approx(X, X_est) assert eigenpy.is_approx(A.dot(X_est), B) -X_sparse = scipy.sparse.random(dim, 10) -B_sparse = A.dot(X_sparse) -B_sparse = B_sparse.tocsc(True) +spqr.analyzePattern(A) +spqr.factorize(A) +X_sparse = spa.random(dim, 10, random_state=rng) +B_sparse = A.dot(X_sparse) +B_sparse: spa.csc_matrix = B_sparse.tocsc(True) if not B_sparse.has_sorted_indices: B_sparse.sort_indices() -X_est = sparseqr.solve(B_sparse) +X_est = spqr.solve(B_sparse) +assert isinstance(X_est, spa.csc_matrix) assert eigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) assert eigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) - -# Others -cols = sparseqr.cols() -rows = sparseqr.rows() - -sparseqr.analyzePattern(A) -sparseqr.factorize(A) - -cols_permutation = sparseqr.colsPermutation() -rank = sparseqr.rank() - -sparseqr.setPivotThreshold(1e-8) diff --git a/unittest/python/test_BDCSVD.py b/unittest/python/test_BDCSVD.py index 1033c8ca3..72c8e51c8 100644 --- a/unittest/python/test_BDCSVD.py +++ b/unittest/python/test_BDCSVD.py @@ -2,47 +2,137 @@ import eigenpy -dim = 100 -rng = np.random.default_rng() +_options = [ + 0, + eigenpy.DecompositionOptions.ComputeThinU, + eigenpy.DecompositionOptions.ComputeThinV, + eigenpy.DecompositionOptions.ComputeFullU, + eigenpy.DecompositionOptions.ComputeFullV, + eigenpy.DecompositionOptions.ComputeThinU + | eigenpy.DecompositionOptions.ComputeThinV, + eigenpy.DecompositionOptions.ComputeFullU + | eigenpy.DecompositionOptions.ComputeFullV, + eigenpy.DecompositionOptions.ComputeThinU + | eigenpy.DecompositionOptions.ComputeFullV, + eigenpy.DecompositionOptions.ComputeFullU + | eigenpy.DecompositionOptions.ComputeThinV, +] -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -opt_U = eigenpy.DecompositionOptions.ComputeFullU -opt_V = eigenpy.DecompositionOptions.ComputeFullV +def test_bdcsvd(options): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -bdcsvd = eigenpy.BDCSVD(A, opt_U | opt_V) -assert bdcsvd.info() == eigenpy.ComputationInfo.Success + bdcsvd = eigenpy.BDCSVD(A, options) + assert bdcsvd.info() == eigenpy.ComputationInfo.Success -# Solve -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = bdcsvd.solve(B) -assert eigenpy.is_approx(X, X_est) -assert eigenpy.is_approx(A.dot(X_est), B) + if options & ( + eigenpy.DecompositionOptions.ComputeThinU + | eigenpy.DecompositionOptions.ComputeFullU + ) and options & ( + eigenpy.DecompositionOptions.ComputeThinV + | eigenpy.DecompositionOptions.ComputeFullV + ): + X = rng.random((dim, 20)) + B = A @ X + X_est = bdcsvd.solve(B) + assert eigenpy.is_approx(X, X_est) + assert eigenpy.is_approx(A @ X_est, B) -# Others -cols = bdcsvd.cols() -rows = bdcsvd.rows() + x = rng.random(dim) + b = A @ x + x_est = bdcsvd.solve(b) + assert eigenpy.is_approx(x, x_est) + assert eigenpy.is_approx(A @ x_est, b) -comp_U = bdcsvd.computeU() -comp_V = bdcsvd.computeV() + rows = bdcsvd.rows() + cols = bdcsvd.cols() + assert cols == dim + assert rows == dim -U = bdcsvd.matrixU() -V = bdcsvd.matrixV() + _bdcsvd_compute = bdcsvd.compute(A) + _bdcsvd_compute_options = bdcsvd.compute(A, options) -nonzerosingval = bdcsvd.nonzeroSingularValues() -singularvalues = bdcsvd.singularValues() + rank = bdcsvd.rank() + singularvalues = bdcsvd.singularValues() + nonzerosingularvalues = bdcsvd.nonzeroSingularValues() + assert rank == nonzerosingularvalues + assert len(singularvalues) == dim + assert all( + singularvalues[i] >= singularvalues[i + 1] + for i in range(len(singularvalues) - 1) + ) -S = np.diag(singularvalues) -V_adj = V.conj().T -assert eigenpy.is_approx(A, U @ S @ V_adj) + compute_u = bdcsvd.computeU() + compute_v = bdcsvd.computeV() + expected_compute_u = bool( + options + & ( + eigenpy.DecompositionOptions.ComputeThinU + | eigenpy.DecompositionOptions.ComputeFullU + ) + ) + expected_compute_v = bool( + options + & ( + eigenpy.DecompositionOptions.ComputeThinV + | eigenpy.DecompositionOptions.ComputeFullV + ) + ) + assert compute_u == expected_compute_u + assert compute_v == expected_compute_v -bdcsvd.setThreshold(1e-8) -threshold = bdcsvd.threshold() + if compute_u: + matrixU = bdcsvd.matrixU() + if options & eigenpy.DecompositionOptions.ComputeFullU: + assert matrixU.shape == (dim, dim) + elif options & eigenpy.DecompositionOptions.ComputeThinU: + assert matrixU.shape == (dim, dim) + assert eigenpy.is_approx(matrixU.T @ matrixU, np.eye(matrixU.shape[1])) -bdcsvd.setThreshold() -threshold = bdcsvd.threshold() + if compute_v: + matrixV = bdcsvd.matrixV() + if options & eigenpy.DecompositionOptions.ComputeFullV: + assert matrixV.shape == (dim, dim) + elif options & eigenpy.DecompositionOptions.ComputeThinV: + assert matrixV.shape == (dim, dim) + assert eigenpy.is_approx(matrixV.T @ matrixV, np.eye(matrixV.shape[1])) -rank = bdcsvd.rank() -bdcsvd.setSwitchSize(10) + if compute_u and compute_v: + U = bdcsvd.matrixU() + V = bdcsvd.matrixV() + S = bdcsvd.singularValues() + S_matrix = np.diag(S) + A_reconstructed = U @ S_matrix @ V.T + assert eigenpy.is_approx(A, A_reconstructed) + + bdcsvd.setSwitchSize(5) + bdcsvd.setSwitchSize(16) + bdcsvd.setSwitchSize(32) + + bdcsvd.setThreshold() + _default_threshold = bdcsvd.threshold() + bdcsvd.setThreshold(1e-8) + assert bdcsvd.threshold() == 1e-8 + + decomp1 = eigenpy.BDCSVD() + decomp2 = eigenpy.BDCSVD() + id1 = decomp1.id() + id2 = decomp2.id() + assert id1 != id2 + assert id1 == decomp1.id() + assert id2 == decomp2.id() + + decomp3 = eigenpy.BDCSVD(dim, dim, options) + decomp4 = eigenpy.BDCSVD(dim, dim, options) + id3 = decomp3.id() + id4 = decomp4.id() + assert id3 != id4 + assert id3 == decomp3.id() + assert id4 == decomp4.id() + + +for opt in _options: + test_bdcsvd(opt) diff --git a/unittest/python/test_FullPivLU.py b/unittest/python/test_FullPivLU.py index f99f3ec61..cdbbc4ad4 100644 --- a/unittest/python/test_FullPivLU.py +++ b/unittest/python/test_FullPivLU.py @@ -4,47 +4,106 @@ dim = 100 rng = np.random.default_rng() - A = rng.random((dim, dim)) A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) - fullpivlu = eigenpy.FullPivLU(A) -# Solve X = rng.random((dim, 20)) B = A.dot(X) X_est = fullpivlu.solve(B) assert eigenpy.is_approx(X, X_est) assert eigenpy.is_approx(A.dot(X_est), B) -# Others -cols = fullpivlu.cols() +x = rng.random(dim) +b = A.dot(x) +x_est = fullpivlu.solve(b) +assert eigenpy.is_approx(x, x_est) +assert eigenpy.is_approx(A.dot(x_est), b) + rows = fullpivlu.rows() +cols = fullpivlu.cols() +assert cols == dim +assert rows == dim -det = fullpivlu.determinant() -dim_kernel = fullpivlu.dimensionOfKernel() -rank = fullpivlu.rank() -rcond = fullpivlu.rcond() -max_pivot = fullpivlu.maxPivot() -nonzero_pivots = fullpivlu.nonzeroPivots() +fullpivlu_compute = fullpivlu.compute(A) +A_reconstructed = fullpivlu.reconstructedMatrix() +assert eigenpy.is_approx(A_reconstructed, A) -is_injective = fullpivlu.isInjective() -is_invertible = fullpivlu.isInvertible() -is_surjective = fullpivlu.isSurjective() +nonzeropivots = fullpivlu.nonzeroPivots() +maxpivot = fullpivlu.maxPivot() +assert nonzeropivots == dim +assert maxpivot > 0 -fullpivlu.setThreshold(1e-8) -threshold = fullpivlu.threshold() +LU = fullpivlu.matrixLU() +P_perm = fullpivlu.permutationP() +Q_perm = fullpivlu.permutationQ() +P = P_perm.toDenseMatrix() +Q = Q_perm.toDenseMatrix() -fullpivlu.setThreshold() -threshold = fullpivlu.threshold() +U = np.triu(LU) +L = np.eye(dim) + np.tril(LU, -1) +assert eigenpy.is_approx(P @ A @ Q, L @ U) + +rank = fullpivlu.rank() +dimkernel = fullpivlu.dimensionOfKernel() +injective = fullpivlu.isInjective() +surjective = fullpivlu.isSurjective() +invertible = fullpivlu.isInvertible() +assert rank == dim +assert dimkernel == 0 +assert injective +assert surjective +assert invertible +kernel = fullpivlu.kernel() +assert eigenpy.is_approx(A @ kernel, np.zeros((dim, 1)), 1e-10) image = fullpivlu.image(A) +assert image.shape[1] == rank + inverse = fullpivlu.inverse() -kernel = fullpivlu.kernel() +assert eigenpy.is_approx(A @ inverse, np.eye(dim)) +assert eigenpy.is_approx(inverse @ A, np.eye(dim)) -LU = fullpivlu.matrixLU() -P = fullpivlu.permutationP() -Q = fullpivlu.permutationQ() +rcond = fullpivlu.rcond() +determinant = fullpivlu.determinant() +det_numpy = np.linalg.det(A) +assert rcond > 0 +assert abs(determinant - det_numpy) / abs(det_numpy) < 1e-10 + +fullpivlu.setThreshold() +default_threshold = fullpivlu.threshold() +fullpivlu.setThreshold(1e-8) +assert fullpivlu.threshold() == 1e-8 + +P_inv = P_perm.inverse().toDenseMatrix() +Q_inv = Q_perm.inverse().toDenseMatrix() +assert eigenpy.is_approx(P @ P_inv, np.eye(dim)) +assert eigenpy.is_approx(Q @ Q_inv, np.eye(dim)) +assert eigenpy.is_approx(P_inv @ P, np.eye(dim)) +assert eigenpy.is_approx(Q_inv @ Q, np.eye(dim)) + +rows_rect = 4 +cols_rect = 6 +A_rect = rng.random((rows_rect, cols_rect)) +fullpivlu_rect = eigenpy.FullPivLU(A_rect) +assert fullpivlu_rect.rows() == rows_rect +assert fullpivlu_rect.cols() == cols_rect +rank_rect = fullpivlu_rect.rank() +assert rank_rect <= min(rows_rect, cols_rect) +assert fullpivlu_rect.dimensionOfKernel() == cols_rect - rank_rect + +decomp1 = eigenpy.FullPivLU() +decomp2 = eigenpy.FullPivLU() +id1 = decomp1.id() +id2 = decomp2.id() +assert id1 != id2 +assert id1 == decomp1.id() +assert id2 == decomp2.id() -reconstructed_matrix = fullpivlu.reconstructedMatrix() -assert eigenpy.is_approx(reconstructed_matrix, A) +decomp3 = eigenpy.FullPivLU(dim, dim) +decomp4 = eigenpy.FullPivLU(dim, dim) +id3 = decomp3.id() +id4 = decomp4.id() +assert id3 != id4 +assert id3 == decomp3.id() +assert id4 == decomp4.id() diff --git a/unittest/python/test_JacobiSVD.py b/unittest/python/test_JacobiSVD.py index 0559c89db..0eb5dcacd 100644 --- a/unittest/python/test_JacobiSVD.py +++ b/unittest/python/test_JacobiSVD.py @@ -2,46 +2,151 @@ import eigenpy -dim = 100 -rng = np.random.default_rng() +_options = [ + 0, + eigenpy.DecompositionOptions.ComputeThinU, + eigenpy.DecompositionOptions.ComputeThinV, + eigenpy.DecompositionOptions.ComputeFullU, + eigenpy.DecompositionOptions.ComputeFullV, + eigenpy.DecompositionOptions.ComputeThinU + | eigenpy.DecompositionOptions.ComputeThinV, + eigenpy.DecompositionOptions.ComputeFullU + | eigenpy.DecompositionOptions.ComputeFullV, + eigenpy.DecompositionOptions.ComputeThinU + | eigenpy.DecompositionOptions.ComputeFullV, + eigenpy.DecompositionOptions.ComputeFullU + | eigenpy.DecompositionOptions.ComputeThinV, +] -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) +_classes = [ + eigenpy.ColPivHhJacobiSVD, + eigenpy.FullPivHhJacobiSVD, + eigenpy.HhJacobiSVD, + eigenpy.NoPrecondJacobiSVD, +] -opt_U = eigenpy.DecompositionOptions.ComputeFullU -opt_V = eigenpy.DecompositionOptions.ComputeFullV -jacobisvd = eigenpy.JacobiSVD(A, opt_U | opt_V) -assert jacobisvd.info() == eigenpy.ComputationInfo.Success +def test_jacobi(cls, options): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -# Solve -X = rng.random((dim, 20)) -B = A.dot(X) -X_est = jacobisvd.solve(B) -assert eigenpy.is_approx(X, X_est) -assert eigenpy.is_approx(A.dot(X_est), B) + if cls == eigenpy.FullPivHhJacobiSVD: + if options != 0 and not ( + options + & ( + eigenpy.DecompositionOptions.ComputeFullU + | eigenpy.DecompositionOptions.ComputeFullV + ) + ): + return -# Others -cols = jacobisvd.cols() -rows = jacobisvd.rows() + jacobisvd = cls(A, options) + assert jacobisvd.info() == eigenpy.ComputationInfo.Success -comp_U = jacobisvd.computeU() -comp_V = jacobisvd.computeV() + if options & ( + eigenpy.DecompositionOptions.ComputeThinU + | eigenpy.DecompositionOptions.ComputeFullU + ) and options & ( + eigenpy.DecompositionOptions.ComputeThinV + | eigenpy.DecompositionOptions.ComputeFullV + ): + X = rng.random((dim, 20)) + B = A @ X + X_est = jacobisvd.solve(B) + assert eigenpy.is_approx(X, X_est) + assert eigenpy.is_approx(A @ X_est, B) -U = jacobisvd.matrixU() -V = jacobisvd.matrixV() + x = rng.random(dim) + b = A @ x + x_est = jacobisvd.solve(b) + assert eigenpy.is_approx(x, x_est) + assert eigenpy.is_approx(A @ x_est, b) -nonzerosingval = jacobisvd.nonzeroSingularValues() -singularvalues = jacobisvd.singularValues() + rows = jacobisvd.rows() + cols = jacobisvd.cols() + assert cols == dim + assert rows == dim -S = np.diag(singularvalues) -V_adj = V.conj().T -assert eigenpy.is_approx(A, U @ S @ V_adj) + _jacobisvd_compute = jacobisvd.compute(A) + _jacobisvd_compute_options = jacobisvd.compute(A, options) -jacobisvd.setThreshold(1e-8) -threshold = jacobisvd.threshold() + rank = jacobisvd.rank() + singularvalues = jacobisvd.singularValues() + nonzerosingularvalues = jacobisvd.nonzeroSingularValues() + assert rank == nonzerosingularvalues + assert len(singularvalues) == dim + assert all( + singularvalues[i] >= singularvalues[i + 1] + for i in range(len(singularvalues) - 1) + ) -jacobisvd.setThreshold() -threshold = jacobisvd.threshold() + compute_u = jacobisvd.computeU() + compute_v = jacobisvd.computeV() + expected_compute_u = bool( + options + & ( + eigenpy.DecompositionOptions.ComputeThinU + | eigenpy.DecompositionOptions.ComputeFullU + ) + ) + expected_compute_v = bool( + options + & ( + eigenpy.DecompositionOptions.ComputeThinV + | eigenpy.DecompositionOptions.ComputeFullV + ) + ) + assert compute_u == expected_compute_u + assert compute_v == expected_compute_v -rank = jacobisvd.rank() + if compute_u: + matrixU = jacobisvd.matrixU() + if options & eigenpy.DecompositionOptions.ComputeFullU: + assert matrixU.shape == (dim, dim) + elif options & eigenpy.DecompositionOptions.ComputeThinU: + assert matrixU.shape == (dim, dim) + assert eigenpy.is_approx(matrixU.T @ matrixU, np.eye(matrixU.shape[1])) + + if compute_v: + matrixV = jacobisvd.matrixV() + if options & eigenpy.DecompositionOptions.ComputeFullV: + assert matrixV.shape == (dim, dim) + elif options & eigenpy.DecompositionOptions.ComputeThinV: + assert matrixV.shape == (dim, dim) + assert eigenpy.is_approx(matrixV.T @ matrixV, np.eye(matrixV.shape[1])) + + if compute_u and compute_v: + U = jacobisvd.matrixU() + V = jacobisvd.matrixV() + S = jacobisvd.singularValues() + S_matrix = np.diag(S) + A_reconstructed = U @ S_matrix @ V.T + assert eigenpy.is_approx(A, A_reconstructed) + + jacobisvd.setThreshold() + _default_threshold = jacobisvd.threshold() + jacobisvd.setThreshold(1e-8) + assert jacobisvd.threshold() == 1e-8 + + decomp1 = cls() + decomp2 = cls() + id1 = decomp1.id() + id2 = decomp2.id() + assert id1 != id2 + assert id1 == decomp1.id() + assert id2 == decomp2.id() + + decomp3 = cls(dim, dim, options) + decomp4 = cls(dim, dim, options) + id3 = decomp3.id() + id4 = decomp4.id() + assert id3 != id4 + assert id3 == decomp3.id() + assert id4 == decomp4.id() + + +for opt in _options: + for cls in _classes: + test_jacobi(cls, opt) diff --git a/unittest/python/test_PartialPivLU.py b/unittest/python/test_PartialPivLU.py index d76f1f039..487a515f5 100644 --- a/unittest/python/test_PartialPivLU.py +++ b/unittest/python/test_PartialPivLU.py @@ -4,28 +4,73 @@ dim = 100 rng = np.random.default_rng() - A = rng.random((dim, dim)) A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) - partialpivlu = eigenpy.PartialPivLU(A) -# Solve X = rng.random((dim, 20)) B = A.dot(X) X_est = partialpivlu.solve(B) assert eigenpy.is_approx(X, X_est) assert eigenpy.is_approx(A.dot(X_est), B) -# Others -cols = partialpivlu.cols() +x = rng.random(dim) +b = A.dot(x) +x_est = partialpivlu.solve(b) +assert eigenpy.is_approx(x, x_est) +assert eigenpy.is_approx(A.dot(x_est), b) + rows = partialpivlu.rows() +cols = partialpivlu.cols() +assert cols == dim +assert rows == dim + +partialpivlu_compute = partialpivlu.compute(A) +A_reconstructed = partialpivlu.reconstructedMatrix() +assert eigenpy.is_approx(A_reconstructed, A) + +LU = partialpivlu.matrixLU() +P_perm = partialpivlu.permutationP() +P = P_perm.toDenseMatrix() + +U = np.triu(LU) +L = np.eye(dim) + np.tril(LU, -1) +assert eigenpy.is_approx(P @ A, L @ U) + +inverse = partialpivlu.inverse() +assert eigenpy.is_approx(A @ inverse, np.eye(dim)) +assert eigenpy.is_approx(inverse @ A, np.eye(dim)) -det = partialpivlu.determinant() rcond = partialpivlu.rcond() +determinant = partialpivlu.determinant() +det_numpy = np.linalg.det(A) +assert rcond > 0 +assert abs(determinant - det_numpy) / abs(det_numpy) < 1e-10 + +P_inv = P_perm.inverse().toDenseMatrix() +assert eigenpy.is_approx(P @ P_inv, np.eye(dim)) +assert eigenpy.is_approx(P_inv @ P, np.eye(dim)) + +decomp1 = eigenpy.PartialPivLU() +decomp2 = eigenpy.PartialPivLU() +id1 = decomp1.id() +id2 = decomp2.id() +assert id1 != id2 +assert id1 == decomp1.id() +assert id2 == decomp2.id() -matrixLU = partialpivlu.matrixLU() -P = partialpivlu.permutationP() +decomp3 = eigenpy.PartialPivLU(dim) +decomp4 = eigenpy.PartialPivLU(dim) +id3 = decomp3.id() +id4 = decomp4.id() +assert id3 != id4 +assert id3 == decomp3.id() +assert id4 == decomp4.id() -reconstructed_matrix = partialpivlu.reconstructedMatrix() -assert eigenpy.is_approx(reconstructed_matrix, A) +decomp5 = eigenpy.PartialPivLU(A) +decomp6 = eigenpy.PartialPivLU(A) +id5 = decomp5.id() +id6 = decomp6.id() +assert id5 != id6 +assert id5 == decomp5.id() +assert id6 == decomp6.id() diff --git a/unittest/python/test_complex_eigen_solver.py b/unittest/python/test_complex_eigen_solver.py index fdf809dba..433696d05 100644 --- a/unittest/python/test_complex_eigen_solver.py +++ b/unittest/python/test_complex_eigen_solver.py @@ -6,15 +6,89 @@ rng = np.random.default_rng() A = rng.random((dim, dim)) -ces = eigenpy.ComplexEigenSolver(A) +ces = eigenpy.ComplexEigenSolver() +es = eigenpy.ComplexEigenSolver(dim) +es = eigenpy.ComplexEigenSolver(A) +assert es.info() == eigenpy.ComputationInfo.Success -assert ces.info() == eigenpy.ComputationInfo.Success +es_with_vectors = eigenpy.ComplexEigenSolver(A, True) +assert es_with_vectors.info() == eigenpy.ComputationInfo.Success -V = ces.eigenvectors() -D = ces.eigenvalues() +es_without_vectors = eigenpy.ComplexEigenSolver(A, False) +assert es_without_vectors.info() == eigenpy.ComputationInfo.Success -assert eigenpy.is_approx(A.dot(V).real, V.dot(np.diag(D)).real) -assert eigenpy.is_approx(A.dot(V).imag, V.dot(np.diag(D)).imag) +V = es.eigenvectors() +D = es.eigenvalues() +assert V.shape == (dim, dim) +assert D.shape == (dim,) -ces.setMaxIterations(10) -assert ces.getMaxIterations() == 10 +AV = A @ V +VD = V @ np.diag(D) +assert eigenpy.is_approx(AV.real, VD.real) +assert eigenpy.is_approx(AV.imag, VD.imag) + +es_compute = eigenpy.ComplexEigenSolver() +result = es_compute.compute(A, False) +assert result.info() == eigenpy.ComputationInfo.Success +D_only = es_compute.eigenvalues() +assert D_only.shape == (dim,) + +result_with_vectors = es_compute.compute(A, True) +assert result_with_vectors.info() == eigenpy.ComputationInfo.Success +V_computed = es_compute.eigenvectors() +D_computed = es_compute.eigenvalues() +assert V_computed.shape == (dim, dim) +assert D_computed.shape == (dim,) + +trace_A = np.trace(A) +trace_D = np.sum(D) +assert abs(trace_A - trace_D.real) < 1e-10 +assert abs(trace_D.imag) < 1e-10 + +es_default = eigenpy.ComplexEigenSolver() +result_default = es_default.compute(A) +assert result_default.info() == eigenpy.ComputationInfo.Success +V_default = es_default.eigenvectors() +D_default = es_default.eigenvalues() + +es_iter = eigenpy.ComplexEigenSolver(A) +default_iter = es_iter.getMaxIterations() +es_iter.setMaxIterations(100) +assert es_iter.getMaxIterations() == 100 +es_iter.setMaxIterations(200) +assert es_iter.getMaxIterations() == 200 + +assert es.info() == eigenpy.ComputationInfo.Success + +ces1 = eigenpy.ComplexEigenSolver() +ces2 = eigenpy.ComplexEigenSolver() +id1 = ces1.id() +id2 = ces2.id() +assert id1 != id2 +assert id1 == ces1.id() +assert id2 == ces2.id() + +dim_constructor = 3 +ces3 = eigenpy.ComplexEigenSolver(dim_constructor) +ces4 = eigenpy.ComplexEigenSolver(dim_constructor) +id3 = ces3.id() +id4 = ces4.id() +assert id3 != id4 +assert id3 == ces3.id() +assert id4 == ces4.id() + +ces5 = eigenpy.ComplexEigenSolver(A) +ces6 = eigenpy.ComplexEigenSolver(A) +id5 = ces5.id() +id6 = ces6.id() +assert id5 != id6 +assert id5 == ces5.id() +assert id6 == ces6.id() + +ces7 = eigenpy.ComplexEigenSolver(A, True) +ces8 = eigenpy.ComplexEigenSolver(A, False) +id7 = ces7.id() +id8 = ces8.id() +assert id7 != id8 +assert id7 == ces7.id() +assert id8 == ces8.id() diff --git a/unittest/python/test_complex_schur.py b/unittest/python/test_complex_schur.py index de5eaa1ad..fe0503307 100644 --- a/unittest/python/test_complex_schur.py +++ b/unittest/python/test_complex_schur.py @@ -2,17 +2,117 @@ import eigenpy -dim = 5 +dim = 100 rng = np.random.default_rng() A = rng.random((dim, dim)) cs = eigenpy.ComplexSchur(A) +assert cs.info() == eigenpy.ComputationInfo.Success U = cs.matrixU() T = cs.matrixT() -U_star = U.conj().T -assert eigenpy.is_approx(A.real, (U @ T @ U_star).real) +A_complex = A.astype(complex) +assert eigenpy.is_approx(A_complex, U @ T @ U.conj().T, 1e-10) +assert eigenpy.is_approx(U @ U.conj().T, np.eye(dim), 1e-10) -cs.setMaxIterations(10) -assert cs.getMaxIterations() == 10 +for row in range(1, dim): + for col in range(row): + assert abs(T[row, col]) < 1e-12 + +A_test = rng.random((dim, dim)) +cs1 = eigenpy.ComplexSchur(dim) +cs1.compute(A_test) +cs2 = eigenpy.ComplexSchur(A_test) + +assert cs1.info() == eigenpy.ComputationInfo.Success +assert cs2.info() == eigenpy.ComputationInfo.Success + +T1 = cs1.matrixT() +U1 = cs1.matrixU() +T2 = cs2.matrixT() +U2 = cs2.matrixU() + +assert eigenpy.is_approx(T1, T2, 1e-12) +assert eigenpy.is_approx(U1, U2, 1e-12) + +cs_no_u = eigenpy.ComplexSchur(A, False) +assert cs_no_u.info() == eigenpy.ComputationInfo.Success +T_no_u = cs_no_u.matrixT() + +assert eigenpy.is_approx(T, T_no_u, 1e-12) + +cs_compute_no_u = eigenpy.ComplexSchur(dim) +result_no_u = cs_compute_no_u.compute(A, False) +assert result_no_u.info() == eigenpy.ComputationInfo.Success +T_compute_no_u = cs_compute_no_u.matrixT() +assert eigenpy.is_approx(T, T_compute_no_u, 1e-12) + +cs_iter = eigenpy.ComplexSchur(dim) +cs_iter.setMaxIterations(30 * dim) # m_maxIterationsPerRow * size +result_iter = cs_iter.compute(A) +assert result_iter.info() == eigenpy.ComputationInfo.Success +assert cs_iter.getMaxIterations() == 30 * dim + +T_iter = cs_iter.matrixT() +U_iter = cs_iter.matrixU() +assert eigenpy.is_approx(T, T_iter, 1e-12) +assert eigenpy.is_approx(U, U_iter, 1e-12) + +cs_few_iter = eigenpy.ComplexSchur(dim) +cs_few_iter.setMaxIterations(1) +result_few = cs_few_iter.compute(A) +assert cs_few_iter.getMaxIterations() == 1 + +A_triangular = np.triu(A) +cs_triangular = eigenpy.ComplexSchur(dim) +cs_triangular.setMaxIterations(1) +result_triangular = cs_triangular.compute(A_triangular) +assert result_triangular.info() == eigenpy.ComputationInfo.Success + +T_triangular = cs_triangular.matrixT() +U_triangular = cs_triangular.matrixU() + +A_triangular_complex = A_triangular.astype(complex) +assert eigenpy.is_approx(T_triangular, A_triangular_complex, 1e-10) +assert eigenpy.is_approx(U_triangular, np.eye(dim, dtype=complex), 1e-10) + +hess = eigenpy.HessenbergDecomposition(A) +H = hess.matrixH() +Q_hess = hess.matrixQ() + +cs_from_hess = eigenpy.ComplexSchur(dim) +result_from_hess = cs_from_hess.computeFromHessenberg(H, Q_hess, True) +assert result_from_hess.info() == eigenpy.ComputationInfo.Success + +T_from_hess = cs_from_hess.matrixT() +U_from_hess = cs_from_hess.matrixU() + +A_complex = A.astype(complex) +assert eigenpy.is_approx( + A_complex, U_from_hess @ T_from_hess @ U_from_hess.conj().T, 1e-10 +) + +cs1_id = eigenpy.ComplexSchur(dim) +cs2_id = eigenpy.ComplexSchur(dim) +id1 = cs1_id.id() +id2 = cs2_id.id() +assert id1 != id2 +assert id1 == cs1_id.id() +assert id2 == cs2_id.id() + +cs3_id = eigenpy.ComplexSchur(A) +cs4_id = eigenpy.ComplexSchur(A) +id3 = cs3_id.id() +id4 = cs4_id.id() +assert id3 != id4 +assert id3 == cs3_id.id() +assert id4 == cs4_id.id() + +cs5_id = eigenpy.ComplexSchur(A, True) +cs6_id = eigenpy.ComplexSchur(A, False) +id5 = cs5_id.id() +id6 = cs6_id.id() +assert id5 != id6 +assert id5 == cs5_id.id() +assert id6 == cs6_id.id() diff --git a/unittest/python/test_generalized_eigen_solver.py b/unittest/python/test_generalized_eigen_solver.py index 21dcd5755..db1dbdb76 100644 --- a/unittest/python/test_generalized_eigen_solver.py +++ b/unittest/python/test_generalized_eigen_solver.py @@ -6,24 +6,112 @@ rng = np.random.default_rng() A = rng.random((dim, dim)) B = rng.random((dim, dim)) -B = (B + B.T) * 0.5 + np.diag(10.0 + rng.random(dim)) # Make B not singular +B = (B + B.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -ges = eigenpy.GeneralizedEigenSolver(A, B) +ges = eigenpy.GeneralizedEigenSolver() +ges_size = eigenpy.GeneralizedEigenSolver(dim) +ges_matrices = eigenpy.GeneralizedEigenSolver(A, B) +assert ges_matrices.info() == eigenpy.ComputationInfo.Success -assert ges.info() == eigenpy.ComputationInfo.Success +ges_with_vectors = eigenpy.GeneralizedEigenSolver(A, B, True) +assert ges_with_vectors.info() == eigenpy.ComputationInfo.Success -alphas = ges.alphas() -betas = ges.betas() +ges_without_vectors = eigenpy.GeneralizedEigenSolver(A, B, False) +assert ges_without_vectors.info() == eigenpy.ComputationInfo.Success -vec = ges.eigenvectors() +alphas = ges_matrices.alphas() +betas = ges_matrices.betas() +eigenvectors = ges_matrices.eigenvectors() +eigenvalues = ges_matrices.eigenvalues() -val_est = alphas / betas -for i in range(dim): - v = vec[:, i] - lam = val_est[i] +assert alphas.shape == (dim,) +assert betas.shape == (dim,) +assert eigenvectors.shape == (dim, dim) +assert eigenvalues.shape == (dim,) + +for k in range(dim): + v = eigenvectors[:, k] + lambda_k = eigenvalues[k] Av = A @ v - lam_Bv = lam * (B @ v) + lambda_Bv = lambda_k * (B @ v) + assert eigenpy.is_approx(Av.real, lambda_Bv.real, 1e-10) + assert eigenpy.is_approx(Av.imag, lambda_Bv.imag, 1e-10) + +for k in range(dim): + v = eigenvectors[:, k] + alpha = alphas[k] + beta = betas[k] + + alpha_Bv = alpha * (B @ v) + beta_Av = beta * (A @ v) + assert eigenpy.is_approx(alpha_Bv.real, beta_Av.real, 1e-10) + assert eigenpy.is_approx(alpha_Bv.imag, beta_Av.imag, 1e-10) + +for k in range(dim): + if abs(betas[k]) > 1e-12: + expected_eigenvalue = alphas[k] / betas[k] + assert abs(eigenvalues[k] - expected_eigenvalue) < 1e-12 + +ges_compute = eigenpy.GeneralizedEigenSolver() +result = ges_compute.compute(A, B, False) +assert result.info() == eigenpy.ComputationInfo.Success +alphas_only = ges_compute.alphas() +betas_only = ges_compute.betas() +eigenvalues_only = ges_compute.eigenvalues() + +result_with_vectors = ges_compute.compute(A, B, True) +assert result_with_vectors.info() == eigenpy.ComputationInfo.Success +eigenvectors_computed = ges_compute.eigenvectors() + +ges_default = eigenpy.GeneralizedEigenSolver() +result_default = ges_default.compute(A, B) +assert result_default.info() == eigenpy.ComputationInfo.Success + +ges_iter = eigenpy.GeneralizedEigenSolver(A, B) +ges_iter.setMaxIterations(100) +ges_iter.setMaxIterations(200) + +A1 = rng.random((dim, dim)) +B1 = rng.random((dim, dim)) +spdA = A.T @ A + A1.T @ A1 +spdB = B.T @ B + B1.T @ B1 + +ges_spd = eigenpy.GeneralizedEigenSolver(spdA, spdB) +assert ges_spd.info() == eigenpy.ComputationInfo.Success + +spd_eigenvalues = ges_spd.eigenvalues() +max_imag = np.max(np.abs(spd_eigenvalues.imag)) +assert max_imag < 1e-10 + +ges1 = eigenpy.GeneralizedEigenSolver() +ges2 = eigenpy.GeneralizedEigenSolver() +id1 = ges1.id() +id2 = ges2.id() +assert id1 != id2 +assert id1 == ges1.id() +assert id2 == ges2.id() + +ges3 = eigenpy.GeneralizedEigenSolver(dim) +ges4 = eigenpy.GeneralizedEigenSolver(dim) +id3 = ges3.id() +id4 = ges4.id() +assert id3 != id4 +assert id3 == ges3.id() +assert id4 == ges4.id() + +ges5 = eigenpy.GeneralizedEigenSolver(A, B) +ges6 = eigenpy.GeneralizedEigenSolver(A, B) +id5 = ges5.id() +id6 = ges6.id() +assert id5 != id6 +assert id5 == ges5.id() +assert id6 == ges6.id() - assert eigenpy.is_approx(Av.real, lam_Bv.real) - assert eigenpy.is_approx(Av.imag, lam_Bv.imag) +ges7 = eigenpy.GeneralizedEigenSolver(A, B, True) +ges8 = eigenpy.GeneralizedEigenSolver(A, B, False) +id7 = ges7.id() +id8 = ges8.id() +assert id7 != id8 +assert id7 == ges7.id() +assert id8 == ges8.id() diff --git a/unittest/python/test_generalized_self_adjoint_eigen_solver.py b/unittest/python/test_generalized_self_adjoint_eigen_solver.py index 2658c777d..a6b95a630 100644 --- a/unittest/python/test_generalized_self_adjoint_eigen_solver.py +++ b/unittest/python/test_generalized_self_adjoint_eigen_solver.py @@ -2,25 +2,114 @@ import eigenpy -dim = 5 -rng = np.random.default_rng() +_options = [ + eigenpy.DecompositionOptions.ComputeEigenvectors + | eigenpy.DecompositionOptions.Ax_lBx, + eigenpy.DecompositionOptions.EigenvaluesOnly | eigenpy.DecompositionOptions.Ax_lBx, + eigenpy.DecompositionOptions.ComputeEigenvectors + | eigenpy.DecompositionOptions.ABx_lx, + eigenpy.DecompositionOptions.EigenvaluesOnly | eigenpy.DecompositionOptions.ABx_lx, + eigenpy.DecompositionOptions.ComputeEigenvectors + | eigenpy.DecompositionOptions.BAx_lx, + eigenpy.DecompositionOptions.EigenvaluesOnly | eigenpy.DecompositionOptions.BAx_lx, +] -A = rng.random((dim, dim)) -A = (A + A.T) * 0.5 -B = rng.random((dim, dim)) -B = B @ B.T + 0.1 * np.eye(dim) +def test_generalized_selfadjoint_eigensolver(options): + dim = 100 + rng = np.random.default_rng() + A = rng.random((dim, dim)) + A = (A + A.T) * 0.5 + B = rng.random((dim, dim)) + B = B @ B.T + 0.1 * np.eye(dim) -gsaes = eigenpy.GeneralizedSelfAdjointEigenSolver(A, B) + gsaes = eigenpy.GeneralizedSelfAdjointEigenSolver(A, B, options) + assert gsaes.info() == eigenpy.ComputationInfo.Success -V = gsaes.eigenvectors() -D = gsaes.eigenvalues() + D = gsaes.eigenvalues() + assert D.shape == (dim,) + assert all(abs(D[i].imag) < 1e-12 for i in range(dim)) + assert all(D[i] <= D[i + 1] + 1e-12 for i in range(dim - 1)) -for i in range(dim): - v = V[:, i] - lam = D[i] + compute_eigenvectors = bool( + options & eigenpy.DecompositionOptions.ComputeEigenvectors + ) - Av = A @ v - lam_Bv = lam * (B @ v) + if compute_eigenvectors: + V = gsaes.eigenvectors() + assert V.shape == (dim, dim) - assert np.allclose(Av, lam_Bv, atol=1e-6) + if options & eigenpy.DecompositionOptions.Ax_lBx: + for i in range(dim): + v = V[:, i] + lam = D[i] + Av = A @ v + lam_Bv = lam * (B @ v) + assert eigenpy.is_approx(Av, lam_Bv, 1e-10) + + VT_B_V = V.T @ B @ V + assert eigenpy.is_approx(VT_B_V, np.eye(dim), 1e-10) + + elif options & eigenpy.DecompositionOptions.ABx_lx: + AB = A @ B + for i in range(dim): + v = V[:, i] + lam = D[i] + ABv = AB @ v + lam_v = lam * v + assert np.allclose(ABv, lam_v) + + elif options & eigenpy.DecompositionOptions.BAx_lx: + BA = B @ A + for i in range(dim): + v = V[:, i] + lam = D[i] + BAv = BA @ v + lam_v = lam * v + assert np.allclose(BAv, lam_v) + + _gsaes_compute = gsaes.compute(A, B) + _gsaes_compute_options = gsaes.compute(A, B, options) + + rank = len([d for d in D if abs(d) > 1e-12]) + assert rank <= dim + + decomp1 = eigenpy.GeneralizedSelfAdjointEigenSolver() + decomp2 = eigenpy.GeneralizedSelfAdjointEigenSolver() + id1 = decomp1.id() + id2 = decomp2.id() + assert id1 != id2 + assert id1 == decomp1.id() + assert id2 == decomp2.id() + + decomp3 = eigenpy.GeneralizedSelfAdjointEigenSolver(dim) + decomp4 = eigenpy.GeneralizedSelfAdjointEigenSolver(dim) + id3 = decomp3.id() + id4 = decomp4.id() + assert id3 != id4 + assert id3 == decomp3.id() + assert id4 == decomp4.id() + + decomp5 = eigenpy.GeneralizedSelfAdjointEigenSolver(A, B, options) + decomp6 = eigenpy.GeneralizedSelfAdjointEigenSolver(A, B, options) + id5 = decomp5.id() + id6 = decomp6.id() + assert id5 != id6 + assert id5 == decomp5.id() + assert id6 == decomp6.id() + + if compute_eigenvectors and (options & eigenpy.DecompositionOptions.Ax_lBx): + A_pos = A @ A.T + np.eye(dim) + gsaes_pos = eigenpy.GeneralizedSelfAdjointEigenSolver(A_pos, B, options) + assert gsaes_pos.info() == eigenpy.ComputationInfo.Success + + D_pos = gsaes_pos.eigenvalues() + if all(D_pos[i] > 1e-12 for i in range(dim)): + sqrt_matrix = gsaes_pos.operatorSqrt() + inv_sqrt_matrix = gsaes_pos.operatorInverseSqrt() + assert sqrt_matrix.shape == (dim, dim) + assert inv_sqrt_matrix.shape == (dim, dim) + + +for opt in _options: + test_generalized_selfadjoint_eigensolver(opt) diff --git a/unittest/python/test_hessenberg_decomposition.py b/unittest/python/test_hessenberg_decomposition.py index 4e1662dd3..449842fb9 100644 --- a/unittest/python/test_hessenberg_decomposition.py +++ b/unittest/python/test_hessenberg_decomposition.py @@ -6,4 +6,66 @@ rng = np.random.default_rng() A = rng.random((dim, dim)) -ges = eigenpy.HessenbergDecomposition(A) +hess = eigenpy.HessenbergDecomposition(A) + +Q = hess.matrixQ() +H = hess.matrixH() + +if np.iscomplexobj(A): + A_reconstructed = Q @ H @ Q.conj().T +else: + A_reconstructed = Q @ H @ Q.T +assert eigenpy.is_approx(A, A_reconstructed, 1e-10) + +for row in range(2, dim): + for col in range(row - 1): + assert abs(H[row, col]) < 1e-12 + +if np.iscomplexobj(Q): + QQ_conj = Q @ Q.conj().T +else: + QQ_conj = Q @ Q.T +assert eigenpy.is_approx(QQ_conj, np.eye(dim), 1e-10) + +A_test = rng.random((dim, dim)) +hess1 = eigenpy.HessenbergDecomposition(dim) +hess1.compute(A_test) +hess2 = eigenpy.HessenbergDecomposition(A_test) + +H1 = hess1.matrixH() +H2 = hess2.matrixH() +Q1 = hess1.matrixQ() +Q2 = hess2.matrixQ() + +assert eigenpy.is_approx(H1, H2, 1e-12) +assert eigenpy.is_approx(Q1, Q2, 1e-12) + +hCoeffs = hess.householderCoefficients() +packed = hess.packedMatrix() + +assert hCoeffs.shape == (dim - 1,) +assert packed.shape == (dim, dim) + +for i in range(dim): + for j in range(i - 1, dim): + if j >= 0: + assert abs(H[i, j] - packed[i, j]) < 1e-12 + +hess_default = eigenpy.HessenbergDecomposition(dim) +hess_matrix = eigenpy.HessenbergDecomposition(A) + +hess1_id = eigenpy.HessenbergDecomposition(dim) +hess2_id = eigenpy.HessenbergDecomposition(dim) +id1 = hess1_id.id() +id2 = hess2_id.id() +assert id1 != id2 +assert id1 == hess1_id.id() +assert id2 == hess2_id.id() + +hess3_id = eigenpy.HessenbergDecomposition(A) +hess4_id = eigenpy.HessenbergDecomposition(A) +id3 = hess3_id.id() +id4 = hess4_id.id() +assert id3 != id4 +assert id3 == hess3_id.id() +assert id4 == hess4_id.id() diff --git a/unittest/python/test_real_qz.py b/unittest/python/test_real_qz.py index cf1fbd1c3..9f5f48d1a 100644 --- a/unittest/python/test_real_qz.py +++ b/unittest/python/test_real_qz.py @@ -8,11 +8,92 @@ B = rng.random((dim, dim)) realqz = eigenpy.RealQZ(A, B) +assert realqz.info() == eigenpy.ComputationInfo.Success Q = realqz.matrixQ() S = realqz.matrixS() Z = realqz.matrixZ() T = realqz.matrixT() -assert eigenpy.is_approx(A, Q @ S @ Z) -assert eigenpy.is_approx(B, Q @ T @ Z) +assert eigenpy.is_approx(A, Q @ S @ Z, 1e-10) +assert eigenpy.is_approx(B, Q @ T @ Z, 1e-10) + +assert eigenpy.is_approx(Q @ Q.T, np.eye(dim), 1e-10) +assert eigenpy.is_approx(Z @ Z.T, np.eye(dim), 1e-10) + +for i in range(dim): + for j in range(i): + assert abs(T[i, j]) < 1e-12 + +for i in range(dim): + for j in range(i - 1): + assert abs(S[i, j]) < 1e-12 + +realqz_no_qz = eigenpy.RealQZ(A, B, False) +assert realqz_no_qz.info() == eigenpy.ComputationInfo.Success +S_no_qz = realqz_no_qz.matrixS() +T_no_qz = realqz_no_qz.matrixT() + +for i in range(dim): + for j in range(i): + assert abs(T_no_qz[i, j]) < 1e-12 + +for i in range(dim): + for j in range(i - 1): + assert abs(S_no_qz[i, j]) < 1e-12 + +realqz_compute_no_qz = eigenpy.RealQZ(dim) +result_no_qz = realqz_compute_no_qz.compute(A, B, False) +assert result_no_qz.info() == eigenpy.ComputationInfo.Success +S_compute_no_qz = realqz_compute_no_qz.matrixS() +T_compute_no_qz = realqz_compute_no_qz.matrixT() + +realqz_with_qz = eigenpy.RealQZ(dim) +realqz_without_qz = eigenpy.RealQZ(dim) + +result_with = realqz_with_qz.compute(A, B, True) +result_without = realqz_without_qz.compute(A, B, False) + +assert result_with.info() == eigenpy.ComputationInfo.Success +assert result_without.info() == eigenpy.ComputationInfo.Success + +S_with = realqz_with_qz.matrixS() +T_with = realqz_with_qz.matrixT() +S_without = realqz_without_qz.matrixS() +T_without = realqz_without_qz.matrixT() + +assert eigenpy.is_approx(S_with, S_without, 1e-12) +assert eigenpy.is_approx(T_with, T_without, 1e-12) + +iterations = realqz.iterations() +assert iterations >= 0 + +realqz_iter = eigenpy.RealQZ(dim) +realqz_iter.setMaxIterations(100) +realqz_iter.setMaxIterations(500) +result_iter = realqz_iter.compute(A, B) +assert result_iter.info() == eigenpy.ComputationInfo.Success + +realqz1_id = eigenpy.RealQZ(dim) +realqz2_id = eigenpy.RealQZ(dim) +id1 = realqz1_id.id() +id2 = realqz2_id.id() +assert id1 != id2 +assert id1 == realqz1_id.id() +assert id2 == realqz2_id.id() + +realqz3_id = eigenpy.RealQZ(A, B) +realqz4_id = eigenpy.RealQZ(A, B) +id3 = realqz3_id.id() +id4 = realqz4_id.id() +assert id3 != id4 +assert id3 == realqz3_id.id() +assert id4 == realqz4_id.id() + +realqz5_id = eigenpy.RealQZ(A, B, True) +realqz6_id = eigenpy.RealQZ(A, B, False) +id5 = realqz5_id.id() +id6 = realqz6_id.id() +assert id5 != id6 +assert id5 == realqz5_id.id() +assert id6 == realqz6_id.id() diff --git a/unittest/python/test_real_schur.py b/unittest/python/test_real_schur.py index a3f5e2811..788d0efc3 100644 --- a/unittest/python/test_real_schur.py +++ b/unittest/python/test_real_schur.py @@ -2,13 +2,129 @@ import eigenpy + +def verify_is_quasi_triangular(T): + size = T.shape[0] + + for row in range(2, size): + for col in range(row - 1): + assert abs(T[row, col]) < 1e-12 + + for row in range(1, size): + if abs(T[row, row - 1]) > 1e-12: + if row < size - 1: + assert abs(T[row + 1, row]) < 1e-12 + + tr = T[row - 1, row - 1] + T[row, row] + det = T[row - 1, row - 1] * T[row, row] - T[row - 1, row] * T[row, row - 1] + assert 4 * det > tr * tr + + dim = 100 rng = np.random.default_rng() A = rng.random((dim, dim)) -cs = eigenpy.RealSchur(A) +rs = eigenpy.RealSchur(A) +assert rs.info() == eigenpy.ComputationInfo.Success + +U = rs.matrixU() +T = rs.matrixT() + +assert eigenpy.is_approx(A, U @ T @ U.T, 1e-10) +assert eigenpy.is_approx(U @ U.T, np.eye(dim), 1e-10) + +verify_is_quasi_triangular(T) + +A_test = rng.random((dim, dim)) +rs1 = eigenpy.RealSchur(dim) +rs1.compute(A_test) +rs2 = eigenpy.RealSchur(A_test) + +assert rs1.info() == eigenpy.ComputationInfo.Success +assert rs2.info() == eigenpy.ComputationInfo.Success + +T1 = rs1.matrixT() +U1 = rs1.matrixU() +T2 = rs2.matrixT() +U2 = rs2.matrixU() + +assert eigenpy.is_approx(T1, T2, 1e-12) +assert eigenpy.is_approx(U1, U2, 1e-12) + +rs_no_u = eigenpy.RealSchur(A, False) +assert rs_no_u.info() == eigenpy.ComputationInfo.Success +T_no_u = rs_no_u.matrixT() + +assert eigenpy.is_approx(T, T_no_u, 1e-12) + +rs_compute_no_u = eigenpy.RealSchur(dim) +result_no_u = rs_compute_no_u.compute(A, False) +assert result_no_u.info() == eigenpy.ComputationInfo.Success +T_compute_no_u = rs_compute_no_u.matrixT() +assert eigenpy.is_approx(T, T_compute_no_u, 1e-12) + +rs_iter = eigenpy.RealSchur(dim) +rs_iter.setMaxIterations(40 * dim) # m_maxIterationsPerRow * size +result_iter = rs_iter.compute(A) +assert result_iter.info() == eigenpy.ComputationInfo.Success +assert rs_iter.getMaxIterations() == 40 * dim + +T_iter = rs_iter.matrixT() +U_iter = rs_iter.matrixU() +assert eigenpy.is_approx(T, T_iter, 1e-12) +assert eigenpy.is_approx(U, U_iter, 1e-12) + +if dim > 2: + rs_few_iter = eigenpy.RealSchur(dim) + rs_few_iter.setMaxIterations(1) + result_few = rs_few_iter.compute(A) + assert rs_few_iter.getMaxIterations() == 1 + +A_triangular = np.triu(A) +rs_triangular = eigenpy.RealSchur(dim) +rs_triangular.setMaxIterations(1) +result_triangular = rs_triangular.compute(A_triangular) +assert result_triangular.info() == eigenpy.ComputationInfo.Success + +T_triangular = rs_triangular.matrixT() +U_triangular = rs_triangular.matrixU() + +assert eigenpy.is_approx(T_triangular, A_triangular, 1e-10) +assert eigenpy.is_approx(U_triangular, np.eye(dim), 1e-10) + +hess = eigenpy.HessenbergDecomposition(A) +H = hess.matrixH() +Q_hess = hess.matrixQ() + +rs_from_hess = eigenpy.RealSchur(dim) +result_from_hess = rs_from_hess.computeFromHessenberg(H, Q_hess, True) +assert result_from_hess.info() == eigenpy.ComputationInfo.Success + +T_from_hess = rs_from_hess.matrixT() +U_from_hess = rs_from_hess.matrixU() + +assert eigenpy.is_approx(A, U_from_hess @ T_from_hess @ U_from_hess.T, 1e-10) + +rs1_id = eigenpy.RealSchur(dim) +rs2_id = eigenpy.RealSchur(dim) +id1 = rs1_id.id() +id2 = rs2_id.id() +assert id1 != id2 +assert id1 == rs1_id.id() +assert id2 == rs2_id.id() -U = cs.matrixU() -T = cs.matrixT() +rs3_id = eigenpy.RealSchur(A) +rs4_id = eigenpy.RealSchur(A) +id3 = rs3_id.id() +id4 = rs4_id.id() +assert id3 != id4 +assert id3 == rs3_id.id() +assert id4 == rs4_id.id() -assert eigenpy.is_approx(A.real, (U @ T @ U.T).real) +rs5_id = eigenpy.RealSchur(A, True) +rs6_id = eigenpy.RealSchur(A, False) +id5 = rs5_id.id() +id6 = rs6_id.id() +assert id5 != id6 +assert id5 == rs5_id.id() +assert id6 == rs6_id.id() diff --git a/unittest/python/test_tridiagonalization.py b/unittest/python/test_tridiagonalization.py index 894301b76..70b43d119 100644 --- a/unittest/python/test_tridiagonalization.py +++ b/unittest/python/test_tridiagonalization.py @@ -5,5 +5,100 @@ dim = 100 rng = np.random.default_rng() A = rng.random((dim, dim)) +A = (A + A.T) * 0.5 tri = eigenpy.Tridiagonalization(A) + +Q = tri.matrixQ() +T = tri.matrixT() + +assert eigenpy.is_approx(A, Q @ T @ Q.T, 1e-10) +assert eigenpy.is_approx(Q @ Q.T, np.eye(dim), 1e-10) + +for i in range(dim): + for j in range(dim): + if abs(i - j) > 1: + assert abs(T[i, j]) < 1e-12 + +assert eigenpy.is_approx(T, T.T, 1e-12) + +diag = tri.diagonal() +sub_diag = tri.subDiagonal() + +for i in range(dim): + assert abs(diag[i] - T[i, i]) < 1e-12 + +for i in range(dim - 1): + assert abs(sub_diag[i] - T[i + 1, i]) < 1e-12 + +A_test = rng.random((dim, dim)) +A_test = (A_test + A_test.T) * 0.5 + +tri1 = eigenpy.Tridiagonalization(dim) +tri1.compute(A_test) +tri2 = eigenpy.Tridiagonalization(A_test) + +Q1 = tri1.matrixQ() +T1 = tri1.matrixT() +Q2 = tri2.matrixQ() +T2 = tri2.matrixT() + +assert eigenpy.is_approx(Q1, Q2, 1e-12) +assert eigenpy.is_approx(T1, T2, 1e-12) + +h_coeffs = tri.householderCoefficients() +packed = tri.packedMatrix() + +assert h_coeffs.shape == (dim - 1,) +assert packed.shape == (dim, dim) + +for i in range(dim): + for j in range(i + 1, dim): + assert abs(packed[i, j] - A[i, j]) < 1e-12 + +for i in range(dim): + assert abs(packed[i, i] - T[i, i]) < 1e-12 + if i < dim - 1: + assert abs(packed[i + 1, i] - T[i + 1, i]) < 1e-12 + +A_diag = np.diag(rng.random(dim)) +tri_diag = eigenpy.Tridiagonalization(A_diag) +Q_diag = tri_diag.matrixQ() +T_diag = tri_diag.matrixT() + +assert eigenpy.is_approx(A_diag, Q_diag @ T_diag @ Q_diag.T, 1e-10) +for i in range(dim): + for j in range(dim): + if i != j: + assert abs(T_diag[i, j]) < 1e-10 + +A_tridiag = np.zeros((dim, dim)) +for i in range(dim): + A_tridiag[i, i] = rng.random() + if i < dim - 1: + val = rng.random() + A_tridiag[i, i + 1] = val + A_tridiag[i + 1, i] = val + +tri_tridiag = eigenpy.Tridiagonalization(A_tridiag) +Q_tridiag = tri_tridiag.matrixQ() +T_tridiag = tri_tridiag.matrixT() + +assert eigenpy.is_approx(A_tridiag, Q_tridiag @ T_tridiag @ Q_tridiag.T, 1e-10) + + +tri1_id = eigenpy.Tridiagonalization(dim) +tri2_id = eigenpy.Tridiagonalization(dim) +id1 = tri1_id.id() +id2 = tri2_id.id() +assert id1 != id2 +assert id1 == tri1_id.id() +assert id2 == tri2_id.id() + +tri3_id = eigenpy.Tridiagonalization(A) +tri4_id = eigenpy.Tridiagonalization(A) +id3 = tri3_id.id() +id4 = tri4_id.id() +assert id3 != id4 +assert id3 == tri3_id.id() +assert id4 == tri4_id.id() From 01e2d7786b019f79b52083294670917a3b7396b1 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Sun, 20 Jul 2025 19:13:06 +0200 Subject: [PATCH 16/43] Add IncompleteCholesky and IncompleteLUT --- CMakeLists.txt | 8 +- .../sparse/IncompleteCholesky.hpp | 121 ++++++++++++++++++ .../decompositions/sparse/IncompleteLUT.hpp | 111 ++++++++++++++++ src/decompositions/decompositions.cpp | 4 + src/decompositions/incomplete-cholesky.cpp | 13 ++ src/decompositions/incomplete-lut.cpp | 13 ++ .../sparse/test_IncompleteCholesky.py | 71 ++++++++++ .../sparse/test_IncompleteLUT.py | 49 +++++++ 8 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 include/eigenpy/decompositions/sparse/IncompleteCholesky.hpp create mode 100644 include/eigenpy/decompositions/sparse/IncompleteLUT.hpp create mode 100644 src/decompositions/incomplete-cholesky.cpp create mode 100644 src/decompositions/incomplete-lut.cpp create mode 100644 unittest/python/decompositions/sparse/test_IncompleteCholesky.py create mode 100644 unittest/python/decompositions/sparse/test_IncompleteLUT.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d665dc08..491a41631 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -213,7 +213,9 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_HEADERS include/eigenpy/decompositions/sparse/LDLT.hpp include/eigenpy/decompositions/sparse/LU.hpp include/eigenpy/decompositions/sparse/SimplicialCholesky.hpp - include/eigenpy/decompositions/sparse/SparseSolverBase.hpp) + include/eigenpy/decompositions/sparse/SparseSolverBase.hpp + include/eigenpy/decompositions/sparse/IncompleteCholesky.hpp + include/eigenpy/decompositions/sparse/IncompleteLUT.hpp) if(BUILD_WITH_CHOLMOD_SUPPORT) list(APPEND ${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_HEADERS @@ -340,7 +342,9 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_SOURCES src/decompositions/self-adjoint-eigen-solver.cpp src/decompositions/permutation-matrix.cpp src/decompositions/simplicial-llt-solver.cpp - src/decompositions/simplicial-ldlt-solver.cpp) + src/decompositions/simplicial-ldlt-solver.cpp + src/decompositions/incomplete-cholesky.cpp + src/decompositions/incomplete-lut.cpp) if(BUILD_WITH_CHOLMOD_SUPPORT) list(APPEND ${PROJECT_NAME}_DECOMPOSITIONS_SOURCES diff --git a/include/eigenpy/decompositions/sparse/IncompleteCholesky.hpp b/include/eigenpy/decompositions/sparse/IncompleteCholesky.hpp new file mode 100644 index 000000000..0786035b3 --- /dev/null +++ b/include/eigenpy/decompositions/sparse/IncompleteCholesky.hpp @@ -0,0 +1,121 @@ +/* + * Copyright 2024 INRIA + */ + +#ifndef __eigenpy_decompositions_sparse_incomplete_cholesky_hpp__ +#define __eigenpy_decompositions_sparse_incomplete_cholesky_hpp__ + +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" + +namespace eigenpy { + +template +struct IncompleteCholeskyVisitor : public boost::python::def_visitor< + IncompleteCholeskyVisitor<_MatrixType>> { + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef typename MatrixType::RealScalar RealScalar; + typedef Eigen::IncompleteCholesky Solver; + typedef Eigen::Matrix + DenseVectorXs; + typedef Eigen::Matrix + DenseMatrixXs; + + typedef Eigen::SparseMatrix FactorType; + typedef Eigen::Matrix VectorRx; + typedef Eigen::PermutationMatrix + PermutationType; + + template + void visit(PyClass& cl) const { + cl.def(bp::init<>(bp::arg("self"), "Default constructor")) + .def(bp::init(bp::args("self", "matrix"), + "Constructs and performs the LDLT " + "factorization from a given matrix.")) + + .def("rows", &Solver::rows, bp::arg("self"), + "Returns the number of rows of the matrix.") + .def("cols", &Solver::cols, bp::arg("self"), + "Returns the number of cols of the matrix.") + + .def("info", &Solver::info, bp::arg("self"), + "Reports whether previous computation was successful.") + + .def("setInitialShift", &Solver::setInitialShift, + bp::args("self", "shift"), "Set the initial shift parameter.") + + .def( + "analyzePattern", + +[](Solver& self, const MatrixType& amat) { + self.analyzePattern(amat); + }, + bp::arg("matrix")) + .def( + "factorize", + +[](Solver& self, const MatrixType& amat) { self.factorize(amat); }, + bp::arg("matrix")) + .def( + "compute", + +[](Solver& self, const MatrixType& amat) { self.compute(amat); }, + bp::arg("matrix")) + + .def( + "matrixL", + +[](Solver& self) -> const FactorType& { return self.matrixL(); }, + bp::return_value_policy()) + .def( + "scalingS", + +[](Solver& self) -> const VectorRx& { return self.scalingS(); }, + bp::return_value_policy()) + .def( + "permutationP", + +[](Solver& self) -> const PermutationType& { + return self.permutationP(); + }, + bp::return_value_policy()) + + .def( + "solve", + +[](Solver const& self, const Eigen::Ref& b) + -> DenseVectorXs { return self.solve(b); }, + bp::arg("b"), + "Returns the solution x of A x = b using the current decomposition " + "of A, where b is a right hand side vector.") + .def( + "solve", + +[](Solver const& self, const Eigen::Ref& B) + -> DenseMatrixXs { return self.solve(B); }, + bp::arg("b"), + "Returns the solution X of A X = B using the current decomposition " + "of A where B is a right hand side matrix.") + .def( + "solve", + +[](Solver const& self, const MatrixType& B) -> MatrixType { + DenseMatrixXs B_dense = DenseMatrixXs(B); + DenseMatrixXs X_dense = self.solve(B_dense); + return MatrixType(X_dense.sparseView()); + }, + bp::arg("b"), + "Returns the solution X of A X = B using the current decomposition " + "of A where B is a right hand side matrix."); + } + + static void expose() { + static const std::string classname = + "IncompleteCholesky_" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string& name) { + bp::class_(name.c_str(), "Incomplete Cholesky.", + bp::no_init) + .def(IncompleteCholeskyVisitor()) + .def(IdVisitor()); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_sparse_incomplete_cholesky_hpp__ diff --git a/include/eigenpy/decompositions/sparse/IncompleteLUT.hpp b/include/eigenpy/decompositions/sparse/IncompleteLUT.hpp new file mode 100644 index 000000000..8eab5f17a --- /dev/null +++ b/include/eigenpy/decompositions/sparse/IncompleteLUT.hpp @@ -0,0 +1,111 @@ +/* + * Copyright 2024 INRIA + */ + +#ifndef __eigenpy_decompositions_sparse_incomplete_lut_hpp__ +#define __eigenpy_decompositions_sparse_incomplete_lut_hpp__ + +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/utils/scalar-name.hpp" + +namespace eigenpy { + +template +struct IncompleteLUTVisitor + : public boost::python::def_visitor< + IncompleteLUTVisitor<_MatrixType>> { + + typedef _MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; + typedef typename MatrixType::RealScalar RealScalar; + typedef Eigen::IncompleteLUT Solver; + typedef Eigen::Matrix + DenseVectorXs; + typedef Eigen::Matrix + DenseMatrixXs; + + template + void visit(PyClass &cl) const { + cl.def(bp::init<>(bp::arg("self"), "Default constructor")) + .def(bp::init(bp::args("self", "matrix"), + "Constructs and performs the LDLT " + "factorization from a given matrix.")) + .def(bp::init( + (bp::arg("matrix"), + bp::arg("droptol") = Eigen::NumTraits::dummy_precision(), + bp::arg("fillfactor") = 10), + "Constructs an incomplete LU factorization from a given matrix.")) + + .def("rows", &Solver::rows, bp::arg("self"), + "Returns the number of rows of the matrix.") + .def("cols", &Solver::cols, bp::arg("self"), + "Returns the number of cols of the matrix.") + + .def("info", &Solver::info, bp::arg("self"), + "Reports whether previous computation was successful.") + + .def( + "analyzePattern", + +[](Solver& self, const MatrixType& amat) { + self.analyzePattern(amat); + }, + bp::arg("matrix")) + .def( + "factorize", + +[](Solver& self, const MatrixType& amat) { self.factorize(amat); }, + bp::arg("matrix")) + .def( + "compute", + +[](Solver& self, const MatrixType& amat) { self.compute(amat); }, + bp::arg("matrix")) + + .def("setDroptol", &Solver::setDroptol, bp::arg("self")) + .def("setFillfactor", &Solver::setFillfactor, bp::arg("self")) + + .def( + "solve", + +[](Solver const& self, const Eigen::Ref& b) + -> DenseVectorXs { return self.solve(b); }, + bp::arg("b"), + "Returns the solution x of A x = b using the current decomposition " + "of A, where b is a right hand side vector.") + .def( + "solve", + +[](Solver const& self, const Eigen::Ref& B) + -> DenseMatrixXs { return self.solve(B); }, + bp::arg("b"), + "Returns the solution X of A X = B using the current decomposition " + "of A where B is a right hand side matrix.") + .def( + "solve", + +[](Solver const& self, const MatrixType& B) -> MatrixType { + DenseMatrixXs B_dense = DenseMatrixXs(B); + DenseMatrixXs X_dense = self.solve(B_dense); + return MatrixType(X_dense.sparseView()); + }, + bp::arg("b"), + "Returns the solution X of A X = B using the current decomposition " + "of A where B is a right hand side matrix.") + ; + } + + static void expose() { + static const std::string classname = + "IncompleteLUT_" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string &name) { + bp::class_( + name.c_str(), + "Incomplete LU factorization with dual-threshold strategy.", + bp::no_init) + .def(IncompleteLUTVisitor()) + .def(IdVisitor()); + } +}; + +} // namespace eigenpy + +#endif // ifndef __eigenpy_decompositions_sparse_incomplete_lut_hpp__ diff --git a/src/decompositions/decompositions.cpp b/src/decompositions/decompositions.cpp index 42197f0fd..a411076d7 100644 --- a/src/decompositions/decompositions.cpp +++ b/src/decompositions/decompositions.cpp @@ -26,6 +26,8 @@ void exposeQRSolvers(); void exposeMINRESSolver(); void exposeSimplicialLLTSolver(); void exposeSimplicialLDLTSolver(); +void exposeIncompleteCholesky(); +void exposeIncompleteLUT(); void exposeSparseLUSolver(); void exposeSparseQRSolver(); void exposePermutationMatrix(); @@ -72,6 +74,8 @@ void exposeDecompositions() { exposeSimplicialLDLTSolver(); exposeSparseLUSolver(); exposeSparseQRSolver(); + exposeIncompleteCholesky(); + exposeIncompleteLUT(); exposePermutationMatrix(); diff --git a/src/decompositions/incomplete-cholesky.cpp b/src/decompositions/incomplete-cholesky.cpp new file mode 100644 index 000000000..f6dcfa036 --- /dev/null +++ b/src/decompositions/incomplete-cholesky.cpp @@ -0,0 +1,13 @@ +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/sparse/IncompleteCholesky.hpp" + +namespace eigenpy { +void exposeIncompleteCholesky() { + using namespace Eigen; + typedef SparseMatrix ColMajorSparseMatrix; + IncompleteCholeskyVisitor::expose("IncompleteCholesky"); +} +} // namespace eigenpy diff --git a/src/decompositions/incomplete-lut.cpp b/src/decompositions/incomplete-lut.cpp new file mode 100644 index 000000000..990e3551c --- /dev/null +++ b/src/decompositions/incomplete-lut.cpp @@ -0,0 +1,13 @@ +/* + * Copyright 2024 INRIA + */ + +#include "eigenpy/decompositions/sparse/IncompleteLUT.hpp" + +namespace eigenpy { +void exposeIncompleteLUT() { + using namespace Eigen; + typedef SparseMatrix ColMajorSparseMatrix; + IncompleteLUTVisitor::expose("IncompleteLUT"); +} +} // namespace eigenpy diff --git a/unittest/python/decompositions/sparse/test_IncompleteCholesky.py b/unittest/python/decompositions/sparse/test_IncompleteCholesky.py new file mode 100644 index 000000000..6f8749234 --- /dev/null +++ b/unittest/python/decompositions/sparse/test_IncompleteCholesky.py @@ -0,0 +1,71 @@ +import numpy as np +from scipy.sparse import csc_matrix + +import eigenpy + +dim = 100 +rng = np.random.default_rng() + +A = rng.random((dim, dim)) +A = (A + A.T) * 0.5 + np.diag(5.0 + rng.random(dim)) +A = csc_matrix(A) + +ichol = eigenpy.IncompleteCholesky(A) +assert ichol.info() == eigenpy.ComputationInfo.Success +assert ichol.rows() == dim +assert ichol.cols() == dim + +X = rng.random((dim, 20)) +B = A.dot(X) +X_est = ichol.solve(B) +assert isinstance(X_est, np.ndarray) +residual = np.linalg.norm(B - A.dot(X_est)) / np.linalg.norm(B) +assert residual < 0.1 + +x = rng.random(dim) +b = A.dot(x) +x_est = ichol.solve(b) +assert isinstance(x_est, np.ndarray) +residual = np.linalg.norm(b - A.dot(x_est)) / np.linalg.norm(b) +assert residual < 0.1 + +X_sparse = csc_matrix(rng.random((dim, 10))) +B_sparse = A.dot(X_sparse).tocsc() +if not B_sparse.has_sorted_indices: + B_sparse.sort_indices() +X_est_sparse = ichol.solve(B_sparse) +assert isinstance(X_est_sparse, csc_matrix) + +ichol.analyzePattern(A) +ichol.factorize(A) +ichol.compute(A) +assert ichol.info() == eigenpy.ComputationInfo.Success + +L = ichol.matrixL() +S_diag = ichol.scalingS() +perm = ichol.permutationP() +P = perm.toDenseMatrix() + +assert isinstance(L, csc_matrix) +assert isinstance(S_diag, np.ndarray) +assert L.shape == (dim, dim) +assert S_diag.shape == (dim,) + +L_dense = L.toarray() +upper_part = np.triu(L_dense, k=1) +assert np.allclose(upper_part, 0, atol=1e-12) + +assert np.all(S_diag > 0) + +S = csc_matrix((S_diag, (range(dim), range(dim))), shape=(dim, dim)) + +PA = P @ A +PAP = PA @ P.T +SPAP = S @ PAP +SPAPS = SPAP @ S + +LLT = L @ L.T + +diff = SPAPS - LLT +relative_error = np.linalg.norm(diff.data) / np.linalg.norm(SPAPS.data) +assert relative_error < 0.5 diff --git a/unittest/python/decompositions/sparse/test_IncompleteLUT.py b/unittest/python/decompositions/sparse/test_IncompleteLUT.py new file mode 100644 index 000000000..0db8bca4d --- /dev/null +++ b/unittest/python/decompositions/sparse/test_IncompleteLUT.py @@ -0,0 +1,49 @@ +import numpy as np +from scipy.sparse import csc_matrix +import eigenpy + +dim = 100 +rng = np.random.default_rng() + +A = rng.random((dim, dim)) +A = (A + A.T) * 0.5 + np.diag(5.0 + rng.random(dim)) +A = csc_matrix(A) + +ilut = eigenpy.IncompleteLUT(A) +assert ilut.info() == eigenpy.ComputationInfo.Success +assert ilut.rows() == dim +assert ilut.cols() == dim + +X = rng.random((dim, 100)) +B = A.dot(X) +X_est = ilut.solve(B) +assert isinstance(X_est, np.ndarray) +residual = np.linalg.norm(B - A.dot(X_est)) / np.linalg.norm(B) +assert residual < 0.1 + +x = rng.random(dim) +b = A.dot(x) +x_est = ilut.solve(b) +assert isinstance(x_est, np.ndarray) +residual = np.linalg.norm(b - A.dot(x_est)) / np.linalg.norm(b) +assert residual < 0.1 + +X_sparse = csc_matrix(rng.random((dim, 10))) +B_sparse = A.dot(X_sparse).tocsc() +if not B_sparse.has_sorted_indices: + B_sparse.sort_indices() +X_est_sparse = ilut.solve(B_sparse) +assert isinstance(X_est_sparse, csc_matrix) + +ilut.analyzePattern(A) +ilut.factorize(A) +assert ilut.info() == eigenpy.ComputationInfo.Success + +ilut_params = eigenpy.IncompleteLUT(A, 1e-4, 15) +assert ilut_params.info() == eigenpy.ComputationInfo.Success + +ilut_set = eigenpy.IncompleteLUT() +ilut_set.setDroptol(1e-3) +ilut_set.setFillfactor(20) +ilut_set.compute(A) +assert ilut_set.info() == eigenpy.ComputationInfo.Success From 0ce8903a938c888ee5e063892a377657f4e74907 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Sun, 20 Jul 2025 20:34:41 +0200 Subject: [PATCH 17/43] Added all preconditioners in solvers --- .../decompositions/sparse/IncompleteLUT.hpp | 87 +++++++++---------- include/eigenpy/solvers/BiCGSTAB.hpp | 6 +- .../solvers/LeastSquaresConjugateGradient.hpp | 9 +- src/solvers/solvers.cpp | 46 +++++++--- unittest/CMakeLists.txt | 6 ++ .../sparse/test_IncompleteLUT.py | 1 + 6 files changed, 90 insertions(+), 65 deletions(-) diff --git a/include/eigenpy/decompositions/sparse/IncompleteLUT.hpp b/include/eigenpy/decompositions/sparse/IncompleteLUT.hpp index 8eab5f17a..5ea730205 100644 --- a/include/eigenpy/decompositions/sparse/IncompleteLUT.hpp +++ b/include/eigenpy/decompositions/sparse/IncompleteLUT.hpp @@ -12,9 +12,7 @@ namespace eigenpy { template struct IncompleteLUTVisitor - : public boost::python::def_visitor< - IncompleteLUTVisitor<_MatrixType>> { - + : public boost::python::def_visitor> { typedef _MatrixType MatrixType; typedef typename MatrixType::Scalar Scalar; typedef typename MatrixType::RealScalar RealScalar; @@ -26,68 +24,67 @@ struct IncompleteLUTVisitor DenseMatrixXs; template - void visit(PyClass &cl) const { + void visit(PyClass& cl) const { cl.def(bp::init<>(bp::arg("self"), "Default constructor")) .def(bp::init(bp::args("self", "matrix"), "Constructs and performs the LDLT " "factorization from a given matrix.")) .def(bp::init( - (bp::arg("matrix"), - bp::arg("droptol") = Eigen::NumTraits::dummy_precision(), - bp::arg("fillfactor") = 10), - "Constructs an incomplete LU factorization from a given matrix.")) + (bp::arg("matrix"), + bp::arg("droptol") = Eigen::NumTraits::dummy_precision(), + bp::arg("fillfactor") = 10), + "Constructs an incomplete LU factorization from a given matrix.")) - .def("rows", &Solver::rows, bp::arg("self"), + .def("rows", &Solver::rows, bp::arg("self"), "Returns the number of rows of the matrix.") - .def("cols", &Solver::cols, bp::arg("self"), + .def("cols", &Solver::cols, bp::arg("self"), "Returns the number of cols of the matrix.") .def("info", &Solver::info, bp::arg("self"), - "Reports whether previous computation was successful.") + "Reports whether previous computation was successful.") .def( - "analyzePattern", - +[](Solver& self, const MatrixType& amat) { - self.analyzePattern(amat); - }, - bp::arg("matrix")) + "analyzePattern", + +[](Solver& self, const MatrixType& amat) { + self.analyzePattern(amat); + }, + bp::arg("matrix")) .def( - "factorize", - +[](Solver& self, const MatrixType& amat) { self.factorize(amat); }, - bp::arg("matrix")) + "factorize", + +[](Solver& self, const MatrixType& amat) { self.factorize(amat); }, + bp::arg("matrix")) .def( - "compute", - +[](Solver& self, const MatrixType& amat) { self.compute(amat); }, - bp::arg("matrix")) + "compute", + +[](Solver& self, const MatrixType& amat) { self.compute(amat); }, + bp::arg("matrix")) .def("setDroptol", &Solver::setDroptol, bp::arg("self")) .def("setFillfactor", &Solver::setFillfactor, bp::arg("self")) .def( - "solve", - +[](Solver const& self, const Eigen::Ref& b) - -> DenseVectorXs { return self.solve(b); }, - bp::arg("b"), - "Returns the solution x of A x = b using the current decomposition " - "of A, where b is a right hand side vector.") + "solve", + +[](Solver const& self, const Eigen::Ref& b) + -> DenseVectorXs { return self.solve(b); }, + bp::arg("b"), + "Returns the solution x of A x = b using the current decomposition " + "of A, where b is a right hand side vector.") .def( - "solve", - +[](Solver const& self, const Eigen::Ref& B) - -> DenseMatrixXs { return self.solve(B); }, - bp::arg("b"), - "Returns the solution X of A X = B using the current decomposition " - "of A where B is a right hand side matrix.") + "solve", + +[](Solver const& self, const Eigen::Ref& B) + -> DenseMatrixXs { return self.solve(B); }, + bp::arg("b"), + "Returns the solution X of A X = B using the current decomposition " + "of A where B is a right hand side matrix.") .def( - "solve", - +[](Solver const& self, const MatrixType& B) -> MatrixType { - DenseMatrixXs B_dense = DenseMatrixXs(B); - DenseMatrixXs X_dense = self.solve(B_dense); - return MatrixType(X_dense.sparseView()); - }, - bp::arg("b"), - "Returns the solution X of A X = B using the current decomposition " - "of A where B is a right hand side matrix.") - ; + "solve", + +[](Solver const& self, const MatrixType& B) -> MatrixType { + DenseMatrixXs B_dense = DenseMatrixXs(B); + DenseMatrixXs X_dense = self.solve(B_dense); + return MatrixType(X_dense.sparseView()); + }, + bp::arg("b"), + "Returns the solution X of A X = B using the current decomposition " + "of A where B is a right hand side matrix."); } static void expose() { @@ -96,7 +93,7 @@ struct IncompleteLUTVisitor expose(classname); } - static void expose(const std::string &name) { + static void expose(const std::string& name) { bp::class_( name.c_str(), "Incomplete LU factorization with dual-threshold strategy.", diff --git a/include/eigenpy/solvers/BiCGSTAB.hpp b/include/eigenpy/solvers/BiCGSTAB.hpp index 3ac824992..af6c56787 100644 --- a/include/eigenpy/solvers/BiCGSTAB.hpp +++ b/include/eigenpy/solvers/BiCGSTAB.hpp @@ -15,7 +15,7 @@ namespace eigenpy { template struct BiCGSTABVisitor : public boost::python::def_visitor> { - typedef Eigen::MatrixXd MatrixType; + typedef typename BiCGSTAB::MatrixType MatrixType; template void visit(PyClass& cl) const { @@ -28,8 +28,8 @@ struct BiCGSTABVisitor "followed by a call to compute().")); } - static void expose() { - bp::class_("BiCGSTAB", bp::no_init) + static void expose(const std::string& name = "BiCGSTAB") { + bp::class_(name.c_str(), bp::no_init) .def(IterativeSolverVisitor()) .def(BiCGSTABVisitor()) .def(IdVisitor()); diff --git a/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp b/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp index 043d3d7fb..420431d2d 100644 --- a/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp +++ b/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp @@ -16,7 +16,7 @@ template struct LeastSquaresConjugateGradientVisitor : public boost::python::def_visitor< LeastSquaresConjugateGradientVisitor> { - typedef Eigen::MatrixXd MatrixType; + typedef typename LeastSquaresConjugateGradient::MatrixType MatrixType; template void visit(PyClass& cl) const { @@ -29,9 +29,10 @@ struct LeastSquaresConjugateGradientVisitor "followed by a call to compute().")); } - static void expose() { - bp::class_( - "LeastSquaresConjugateGradient", bp::no_init) + static void expose( + const std::string& name = "LeastSquaresConjugateGradient") { + bp::class_(name.c_str(), + bp::no_init) .def(IterativeSolverVisitor()) .def(LeastSquaresConjugateGradientVisitor< LeastSquaresConjugateGradient>()) diff --git a/src/solvers/solvers.cpp b/src/solvers/solvers.cpp index 4c1fcad62..10bb86269 100644 --- a/src/solvers/solvers.cpp +++ b/src/solvers/solvers.cpp @@ -18,23 +18,43 @@ namespace eigenpy { void exposeSolvers() { using namespace Eigen; - ConjugateGradientVisitor< - ConjugateGradient>::expose(); + + using Eigen::Lower; + using Eigen::Upper; + + using Eigen::BiCGSTAB; + using Eigen::ConjugateGradient; + using Eigen::LeastSquaresConjugateGradient; + + using Eigen::DiagonalPreconditioner; + using Eigen::IdentityPreconditioner; + using Eigen::LeastSquareDiagonalPreconditioner; + + using IdentityBiCGSTAB = BiCGSTAB; + using IdentityConjugateGradient = + ConjugateGradient; + using IdentityLeastSquaresConjugateGradient = + LeastSquaresConjugateGradient; + using DiagonalLeastSquaresConjugateGradient = LeastSquaresConjugateGradient< + MatrixXd, DiagonalPreconditioner>; + + ConjugateGradientVisitor>::expose( + "ConjugateGradient"); + ConjugateGradientVisitor::expose( + "IdentityConjugateGradient"); + #if EIGEN_VERSION_AT_LEAST(3, 3, 5) LeastSquaresConjugateGradientVisitor>>::expose(); + MatrixXd, LeastSquareDiagonalPreconditioner>>:: + expose("LeastSquaresConjugateGradient"); + LeastSquaresConjugateGradientVisitor:: + expose("IdentityLeastSquaresConjugateGradient"); + LeastSquaresConjugateGradientVisitor:: + expose("DiagonalLeastSquaresConjugateGradient"); #endif - // Conjugate gradient with limited BFGS preconditioner - ConjugateGradientVisitor< - ConjugateGradient>:: - expose("IdentityConjugateGradient"); - // ConjugateGradientVisitor< - // ConjugateGradient - // > >::expose("LimitedBFGSConjugateGradient"); - - BiCGSTABVisitor>>::expose(); + BiCGSTABVisitor>::expose("BiCGSTAB"); + BiCGSTABVisitor::expose("IdentityBiCGSTAB"); } } // namespace eigenpy diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 90eb2a97d..df147012a 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -219,6 +219,12 @@ if(BUILD_TESTING_SCIPY) add_python_eigenpy_unit_test( "py-SimplicialLDLT" "unittest/python/decompositions/sparse/test_SimplicialLDLT.py") + add_python_eigenpy_unit_test( + "py-IncompleteCholesky" + "unittest/python/decompositions/sparse/test_IncompleteCholesky.py") + add_python_eigenpy_unit_test( + "py-IncompleteLUT" + "unittest/python/decompositions/sparse/test_IncompleteLUT.py") add_python_eigenpy_unit_test( "py-SparseLU" "unittest/python/decompositions/sparse/test_SparseLU.py") add_python_eigenpy_unit_test( diff --git a/unittest/python/decompositions/sparse/test_IncompleteLUT.py b/unittest/python/decompositions/sparse/test_IncompleteLUT.py index 0db8bca4d..13ea6d164 100644 --- a/unittest/python/decompositions/sparse/test_IncompleteLUT.py +++ b/unittest/python/decompositions/sparse/test_IncompleteLUT.py @@ -1,5 +1,6 @@ import numpy as np from scipy.sparse import csc_matrix + import eigenpy dim = 100 From c187974799e6b3bc18aee0c0cefb62138ebce780 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Sun, 20 Jul 2025 22:45:33 +0200 Subject: [PATCH 18/43] Clean code: Set the same nomenclature of files for exposed classes and put MINRES in capital letters --- CMakeLists.txt | 2 +- .../decompositions/{minres.hpp => MINRES.hpp} | 0 src/decompositions/minres-solver.cpp | 2 +- unittest/CMakeLists.txt | 38 +++++++++---------- ...n_solver.py => test_ComplexEigenSolver.py} | 0 ..._complex_schur.py => test_ComplexSchur.py} | 0 ...st_eigen_solver.py => test_EigenSolver.py} | 0 ...lver.py => test_GeneralizedEigenSolver.py} | 0 ...test_GeneralizedSelfAdjointEigenSolver.py} | 0 .../{test_geometry.py => test_Geometry.py} | 0 ...ion.py => test_HessenbergDecomposition.py} | 0 .../{test_real_qz.py => test_RealQZ.py} | 0 .../{test_real_schur.py => test_RealSchur.py} | 0 ...lver.py => test_SelfAdjointEigenSolver.py} | 0 ...lization.py => test_Tridiagonalization.py} | 0 15 files changed, 20 insertions(+), 22 deletions(-) rename include/eigenpy/decompositions/{minres.hpp => MINRES.hpp} (100%) rename unittest/python/{test_complex_eigen_solver.py => test_ComplexEigenSolver.py} (100%) rename unittest/python/{test_complex_schur.py => test_ComplexSchur.py} (100%) rename unittest/python/{test_eigen_solver.py => test_EigenSolver.py} (100%) rename unittest/python/{test_generalized_eigen_solver.py => test_GeneralizedEigenSolver.py} (100%) rename unittest/python/{test_generalized_self_adjoint_eigen_solver.py => test_GeneralizedSelfAdjointEigenSolver.py} (100%) rename unittest/python/{test_geometry.py => test_Geometry.py} (100%) rename unittest/python/{test_hessenberg_decomposition.py => test_HessenbergDecomposition.py} (100%) rename unittest/python/{test_real_qz.py => test_RealQZ.py} (100%) rename unittest/python/{test_real_schur.py => test_RealSchur.py} (100%) rename unittest/python/{test_self_adjoint_eigen_solver.py => test_SelfAdjointEigenSolver.py} (100%) rename unittest/python/{test_tridiagonalization.py => test_Tridiagonalization.py} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 491a41631..63de3f80e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -248,7 +248,7 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_HEADERS include/eigenpy/decompositions/CompleteOrthogonalDecomposition.hpp include/eigenpy/decompositions/FullPivHouseholderQR.hpp include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp - include/eigenpy/decompositions/minres.hpp + include/eigenpy/decompositions/MINRES.hpp include/eigenpy/decompositions/SVDBase.hpp include/eigenpy/decompositions/BDCSVD.hpp include/eigenpy/decompositions/JacobiSVD.hpp) diff --git a/include/eigenpy/decompositions/minres.hpp b/include/eigenpy/decompositions/MINRES.hpp similarity index 100% rename from include/eigenpy/decompositions/minres.hpp rename to include/eigenpy/decompositions/MINRES.hpp diff --git a/src/decompositions/minres-solver.cpp b/src/decompositions/minres-solver.cpp index 44a12cea1..050c7f7ac 100644 --- a/src/decompositions/minres-solver.cpp +++ b/src/decompositions/minres-solver.cpp @@ -2,7 +2,7 @@ * Copyright 2024 INRIA */ -#include "eigenpy/decompositions/minres.hpp" +#include "eigenpy/decompositions/MINRES.hpp" namespace eigenpy { void exposeMINRESSolver() { diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index df147012a..e3dc2b5f1 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -117,7 +117,7 @@ add_python_lib_unit_test("py-multiple-registration" "unittest/python/test_multiple_registration.py") add_python_lib_unit_test("py-tensor" "unittest/python/test_tensor.py") -add_python_lib_unit_test("py-geometry" "unittest/python/test_geometry.py") +add_python_lib_unit_test("py-Geometry" "unittest/python/test_Geometry.py") add_python_lib_unit_test("py-complex" "unittest/python/test_complex.py") add_python_lib_unit_test("py-deprecation-policy" "unittest/python/test_deprecation_policy.py") @@ -134,38 +134,36 @@ add_python_eigenpy_lib_unit_test("py-dimensions" add_python_eigenpy_lib_unit_test("py-version" "unittest/python/test_version.py") -add_python_eigenpy_lib_unit_test("py-eigen-solver" - "unittest/python/test_eigen_solver.py") +add_python_eigenpy_lib_unit_test("py-EigenSolver" + "unittest/python/test_EigenSolver.py") add_python_eigenpy_lib_unit_test( - "py-generalized-eigen-solver" - "unittest/python/test_generalized_eigen_solver.py") + "py-GeneralizedEigenSolver" "unittest/python/test_GeneralizedEigenSolver.py") add_python_eigenpy_lib_unit_test( - "py-generalized-self-adjoint-eigen-solver" - "unittest/python/test_generalized_self_adjoint_eigen_solver.py") + "py-GeneralizedSelfAdjointEigenSolver" + "unittest/python/test_GeneralizedSelfAdjointEigenSolver.py") -add_python_eigenpy_lib_unit_test("py-complex-eigen-solver" - "unittest/python/test_complex_eigen_solver.py") +add_python_eigenpy_lib_unit_test("py-ComplexEigenSolver" + "unittest/python/test_ComplexEigenSolver.py") -add_python_eigenpy_lib_unit_test("py-complex-schur" - "unittest/python/test_complex_schur.py") +add_python_eigenpy_lib_unit_test("py-ComplexSchur" + "unittest/python/test_ComplexSchur.py") add_python_eigenpy_lib_unit_test( - "py-self-adjoint-eigen-solver" - "unittest/python/test_self_adjoint_eigen_solver.py") + "py-SelfAdjointEigenSolver" "unittest/python/test_SelfAdjointEigenSolver.py") add_python_eigenpy_lib_unit_test( - "py-hessenberg-decomposition" - "unittest/python/test_hessenberg_decomposition.py") + "py-HessenbergDecomposition" + "unittest/python/test_HessenbergDecomposition.py") -add_python_eigenpy_lib_unit_test("py-real-qz" "unittest/python/test_real_qz.py") +add_python_eigenpy_lib_unit_test("py-RealQZ" "unittest/python/test_RealQZ.py") -add_python_eigenpy_lib_unit_test("py-real-schur" - "unittest/python/test_real_schur.py") +add_python_eigenpy_lib_unit_test("py-RealSchur" + "unittest/python/test_RealSchur.py") -add_python_eigenpy_lib_unit_test("py-tridiagonalization" - "unittest/python/test_tridiagonalization.py") +add_python_eigenpy_lib_unit_test("py-Tridiagonalization" + "unittest/python/test_Tridiagonalization.py") add_python_eigenpy_lib_unit_test("py-LLT" "unittest/python/test_LLT.py") diff --git a/unittest/python/test_complex_eigen_solver.py b/unittest/python/test_ComplexEigenSolver.py similarity index 100% rename from unittest/python/test_complex_eigen_solver.py rename to unittest/python/test_ComplexEigenSolver.py diff --git a/unittest/python/test_complex_schur.py b/unittest/python/test_ComplexSchur.py similarity index 100% rename from unittest/python/test_complex_schur.py rename to unittest/python/test_ComplexSchur.py diff --git a/unittest/python/test_eigen_solver.py b/unittest/python/test_EigenSolver.py similarity index 100% rename from unittest/python/test_eigen_solver.py rename to unittest/python/test_EigenSolver.py diff --git a/unittest/python/test_generalized_eigen_solver.py b/unittest/python/test_GeneralizedEigenSolver.py similarity index 100% rename from unittest/python/test_generalized_eigen_solver.py rename to unittest/python/test_GeneralizedEigenSolver.py diff --git a/unittest/python/test_generalized_self_adjoint_eigen_solver.py b/unittest/python/test_GeneralizedSelfAdjointEigenSolver.py similarity index 100% rename from unittest/python/test_generalized_self_adjoint_eigen_solver.py rename to unittest/python/test_GeneralizedSelfAdjointEigenSolver.py diff --git a/unittest/python/test_geometry.py b/unittest/python/test_Geometry.py similarity index 100% rename from unittest/python/test_geometry.py rename to unittest/python/test_Geometry.py diff --git a/unittest/python/test_hessenberg_decomposition.py b/unittest/python/test_HessenbergDecomposition.py similarity index 100% rename from unittest/python/test_hessenberg_decomposition.py rename to unittest/python/test_HessenbergDecomposition.py diff --git a/unittest/python/test_real_qz.py b/unittest/python/test_RealQZ.py similarity index 100% rename from unittest/python/test_real_qz.py rename to unittest/python/test_RealQZ.py diff --git a/unittest/python/test_real_schur.py b/unittest/python/test_RealSchur.py similarity index 100% rename from unittest/python/test_real_schur.py rename to unittest/python/test_RealSchur.py diff --git a/unittest/python/test_self_adjoint_eigen_solver.py b/unittest/python/test_SelfAdjointEigenSolver.py similarity index 100% rename from unittest/python/test_self_adjoint_eigen_solver.py rename to unittest/python/test_SelfAdjointEigenSolver.py diff --git a/unittest/python/test_tridiagonalization.py b/unittest/python/test_Tridiagonalization.py similarity index 100% rename from unittest/python/test_tridiagonalization.py rename to unittest/python/test_Tridiagonalization.py From 5e12ee17f45db0383786910ccde8756a3b91cac6 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Sun, 20 Jul 2025 23:02:19 +0200 Subject: [PATCH 19/43] Set Eigen::Lower everywhere for UpLo template option --- src/solvers/solvers.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/solvers/solvers.cpp b/src/solvers/solvers.cpp index 10bb86269..770700a4a 100644 --- a/src/solvers/solvers.cpp +++ b/src/solvers/solvers.cpp @@ -20,7 +20,6 @@ void exposeSolvers() { using namespace Eigen; using Eigen::Lower; - using Eigen::Upper; using Eigen::BiCGSTAB; using Eigen::ConjugateGradient; @@ -32,13 +31,13 @@ void exposeSolvers() { using IdentityBiCGSTAB = BiCGSTAB; using IdentityConjugateGradient = - ConjugateGradient; + ConjugateGradient; using IdentityLeastSquaresConjugateGradient = LeastSquaresConjugateGradient; using DiagonalLeastSquaresConjugateGradient = LeastSquaresConjugateGradient< MatrixXd, DiagonalPreconditioner>; - ConjugateGradientVisitor>::expose( + ConjugateGradientVisitor>::expose( "ConjugateGradient"); ConjugateGradientVisitor::expose( "IdentityConjugateGradient"); From 4c749d695fb8375835cdfad9f5c5ce6304bbe8a6 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Sun, 20 Jul 2025 23:23:22 +0200 Subject: [PATCH 20/43] Switch MINRES, IncompleteLUT and IncompleteCholesky in eigenpy.solvers scope --- CMakeLists.txt | 15 ++++++--------- .../sparse => solvers}/IncompleteCholesky.hpp | 0 .../sparse => solvers}/IncompleteLUT.hpp | 0 .../{decompositions => solvers}/MINRES.hpp | 0 src/decompositions/decompositions.cpp | 6 ------ src/decompositions/incomplete-cholesky.cpp | 13 ------------- src/decompositions/incomplete-lut.cpp | 13 ------------- src/decompositions/minres-solver.cpp | 12 ------------ src/solvers/solvers.cpp | 10 ++++++++++ unittest/CMakeLists.txt | 10 ++++------ .../sparse => }/test_IncompleteCholesky.py | 2 +- .../sparse => }/test_IncompleteLUT.py | 6 +++--- unittest/python/test_MINRES.py | 2 +- 13 files changed, 25 insertions(+), 64 deletions(-) rename include/eigenpy/{decompositions/sparse => solvers}/IncompleteCholesky.hpp (100%) rename include/eigenpy/{decompositions/sparse => solvers}/IncompleteLUT.hpp (100%) rename include/eigenpy/{decompositions => solvers}/MINRES.hpp (100%) delete mode 100644 src/decompositions/incomplete-cholesky.cpp delete mode 100644 src/decompositions/incomplete-lut.cpp delete mode 100644 src/decompositions/minres-solver.cpp rename unittest/python/{decompositions/sparse => }/test_IncompleteCholesky.py (97%) rename unittest/python/{decompositions/sparse => }/test_IncompleteLUT.py (89%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 63de3f80e..7ccc9f4b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,10 +191,13 @@ set(${PROJECT_NAME}_SOLVERS_HEADERS include/eigenpy/solvers/IterativeSolverBase.hpp include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp include/eigenpy/solvers/BiCGSTAB.hpp + include/eigenpy/solvers/MINRES.hpp include/eigenpy/solvers/ConjugateGradient.hpp include/eigenpy/solvers/SparseSolverBase.hpp include/eigenpy/solvers/BasicPreconditioners.hpp - include/eigenpy/solvers/BFGSPreconditioners.hpp) + include/eigenpy/solvers/BFGSPreconditioners.hpp + include/eigenpy/solvers/IncompleteCholesky.hpp + include/eigenpy/solvers/IncompleteLUT.hpp) set(${PROJECT_NAME}_EIGEN_HEADERS include/eigenpy/eigen/EigenBase.hpp) @@ -213,9 +216,7 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_HEADERS include/eigenpy/decompositions/sparse/LDLT.hpp include/eigenpy/decompositions/sparse/LU.hpp include/eigenpy/decompositions/sparse/SimplicialCholesky.hpp - include/eigenpy/decompositions/sparse/SparseSolverBase.hpp - include/eigenpy/decompositions/sparse/IncompleteCholesky.hpp - include/eigenpy/decompositions/sparse/IncompleteLUT.hpp) + include/eigenpy/decompositions/sparse/SparseSolverBase.hpp) if(BUILD_WITH_CHOLMOD_SUPPORT) list(APPEND ${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_HEADERS @@ -248,7 +249,6 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_HEADERS include/eigenpy/decompositions/CompleteOrthogonalDecomposition.hpp include/eigenpy/decompositions/FullPivHouseholderQR.hpp include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp - include/eigenpy/decompositions/MINRES.hpp include/eigenpy/decompositions/SVDBase.hpp include/eigenpy/decompositions/BDCSVD.hpp include/eigenpy/decompositions/JacobiSVD.hpp) @@ -335,16 +335,13 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_SOURCES src/decompositions/tridiagonalization.cpp src/decompositions/real-schur.cpp src/decompositions/partialpivlu-solver.cpp - src/decompositions/minres-solver.cpp src/decompositions/sparse-lu-solver.cpp src/decompositions/sparse-qr-solver.cpp src/decompositions/qr-solvers.cpp src/decompositions/self-adjoint-eigen-solver.cpp src/decompositions/permutation-matrix.cpp src/decompositions/simplicial-llt-solver.cpp - src/decompositions/simplicial-ldlt-solver.cpp - src/decompositions/incomplete-cholesky.cpp - src/decompositions/incomplete-lut.cpp) + src/decompositions/simplicial-ldlt-solver.cpp) if(BUILD_WITH_CHOLMOD_SUPPORT) list(APPEND ${PROJECT_NAME}_DECOMPOSITIONS_SOURCES diff --git a/include/eigenpy/decompositions/sparse/IncompleteCholesky.hpp b/include/eigenpy/solvers/IncompleteCholesky.hpp similarity index 100% rename from include/eigenpy/decompositions/sparse/IncompleteCholesky.hpp rename to include/eigenpy/solvers/IncompleteCholesky.hpp diff --git a/include/eigenpy/decompositions/sparse/IncompleteLUT.hpp b/include/eigenpy/solvers/IncompleteLUT.hpp similarity index 100% rename from include/eigenpy/decompositions/sparse/IncompleteLUT.hpp rename to include/eigenpy/solvers/IncompleteLUT.hpp diff --git a/include/eigenpy/decompositions/MINRES.hpp b/include/eigenpy/solvers/MINRES.hpp similarity index 100% rename from include/eigenpy/decompositions/MINRES.hpp rename to include/eigenpy/solvers/MINRES.hpp diff --git a/src/decompositions/decompositions.cpp b/src/decompositions/decompositions.cpp index a411076d7..d982a5763 100644 --- a/src/decompositions/decompositions.cpp +++ b/src/decompositions/decompositions.cpp @@ -23,11 +23,8 @@ void exposeLDLTSolver(); void exposeFullPivLUSolver(); void exposePartialPivLUSolver(); void exposeQRSolvers(); -void exposeMINRESSolver(); void exposeSimplicialLLTSolver(); void exposeSimplicialLDLTSolver(); -void exposeIncompleteCholesky(); -void exposeIncompleteLUT(); void exposeSparseLUSolver(); void exposeSparseQRSolver(); void exposePermutationMatrix(); @@ -52,7 +49,6 @@ void exposeDecompositions() { exposeFullPivLUSolver(); exposePartialPivLUSolver(); exposeQRSolvers(); - exposeMINRESSolver(); exposeBDCSVDSolver(); exposeJacobiSVDSolver(); @@ -74,8 +70,6 @@ void exposeDecompositions() { exposeSimplicialLDLTSolver(); exposeSparseLUSolver(); exposeSparseQRSolver(); - exposeIncompleteCholesky(); - exposeIncompleteLUT(); exposePermutationMatrix(); diff --git a/src/decompositions/incomplete-cholesky.cpp b/src/decompositions/incomplete-cholesky.cpp deleted file mode 100644 index f6dcfa036..000000000 --- a/src/decompositions/incomplete-cholesky.cpp +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2024 INRIA - */ - -#include "eigenpy/decompositions/sparse/IncompleteCholesky.hpp" - -namespace eigenpy { -void exposeIncompleteCholesky() { - using namespace Eigen; - typedef SparseMatrix ColMajorSparseMatrix; - IncompleteCholeskyVisitor::expose("IncompleteCholesky"); -} -} // namespace eigenpy diff --git a/src/decompositions/incomplete-lut.cpp b/src/decompositions/incomplete-lut.cpp deleted file mode 100644 index 990e3551c..000000000 --- a/src/decompositions/incomplete-lut.cpp +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2024 INRIA - */ - -#include "eigenpy/decompositions/sparse/IncompleteLUT.hpp" - -namespace eigenpy { -void exposeIncompleteLUT() { - using namespace Eigen; - typedef SparseMatrix ColMajorSparseMatrix; - IncompleteLUTVisitor::expose("IncompleteLUT"); -} -} // namespace eigenpy diff --git a/src/decompositions/minres-solver.cpp b/src/decompositions/minres-solver.cpp deleted file mode 100644 index 050c7f7ac..000000000 --- a/src/decompositions/minres-solver.cpp +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright 2024 INRIA - */ - -#include "eigenpy/decompositions/MINRES.hpp" - -namespace eigenpy { -void exposeMINRESSolver() { - using namespace Eigen; - MINRESSolverVisitor::expose("MINRES"); -} -} // namespace eigenpy diff --git a/src/solvers/solvers.cpp b/src/solvers/solvers.cpp index 770700a4a..58d39f350 100644 --- a/src/solvers/solvers.cpp +++ b/src/solvers/solvers.cpp @@ -14,6 +14,10 @@ #endif #include "eigenpy/solvers/BiCGSTAB.hpp" +#include "eigenpy/solvers/MINRES.hpp" + +#include "eigenpy/solvers/IncompleteLUT.hpp" +#include "eigenpy/solvers/IncompleteCholesky.hpp" namespace eigenpy { void exposeSolvers() { @@ -54,6 +58,12 @@ void exposeSolvers() { BiCGSTABVisitor>::expose("BiCGSTAB"); BiCGSTABVisitor::expose("IdentityBiCGSTAB"); + + MINRESSolverVisitor::expose("MINRES"); + + typedef SparseMatrix ColMajorSparseMatrix; + IncompleteLUTVisitor::expose("IncompleteLUT"); + IncompleteCholeskyVisitor::expose("IncompleteCholesky"); } } // namespace eigenpy diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index e3dc2b5f1..130b3e1e2 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -217,12 +217,10 @@ if(BUILD_TESTING_SCIPY) add_python_eigenpy_unit_test( "py-SimplicialLDLT" "unittest/python/decompositions/sparse/test_SimplicialLDLT.py") - add_python_eigenpy_unit_test( - "py-IncompleteCholesky" - "unittest/python/decompositions/sparse/test_IncompleteCholesky.py") - add_python_eigenpy_unit_test( - "py-IncompleteLUT" - "unittest/python/decompositions/sparse/test_IncompleteLUT.py") + add_python_eigenpy_unit_test("py-IncompleteCholesky" + "unittest/python/test_IncompleteCholesky.py") + add_python_eigenpy_unit_test("py-IncompleteLUT" + "unittest/python/test_IncompleteLUT.py") add_python_eigenpy_unit_test( "py-SparseLU" "unittest/python/decompositions/sparse/test_SparseLU.py") add_python_eigenpy_unit_test( diff --git a/unittest/python/decompositions/sparse/test_IncompleteCholesky.py b/unittest/python/test_IncompleteCholesky.py similarity index 97% rename from unittest/python/decompositions/sparse/test_IncompleteCholesky.py rename to unittest/python/test_IncompleteCholesky.py index 6f8749234..b634f8b90 100644 --- a/unittest/python/decompositions/sparse/test_IncompleteCholesky.py +++ b/unittest/python/test_IncompleteCholesky.py @@ -10,7 +10,7 @@ A = (A + A.T) * 0.5 + np.diag(5.0 + rng.random(dim)) A = csc_matrix(A) -ichol = eigenpy.IncompleteCholesky(A) +ichol = eigenpy.solvers.IncompleteCholesky(A) assert ichol.info() == eigenpy.ComputationInfo.Success assert ichol.rows() == dim assert ichol.cols() == dim diff --git a/unittest/python/decompositions/sparse/test_IncompleteLUT.py b/unittest/python/test_IncompleteLUT.py similarity index 89% rename from unittest/python/decompositions/sparse/test_IncompleteLUT.py rename to unittest/python/test_IncompleteLUT.py index 13ea6d164..0ff820ef7 100644 --- a/unittest/python/decompositions/sparse/test_IncompleteLUT.py +++ b/unittest/python/test_IncompleteLUT.py @@ -10,7 +10,7 @@ A = (A + A.T) * 0.5 + np.diag(5.0 + rng.random(dim)) A = csc_matrix(A) -ilut = eigenpy.IncompleteLUT(A) +ilut = eigenpy.solvers.IncompleteLUT(A) assert ilut.info() == eigenpy.ComputationInfo.Success assert ilut.rows() == dim assert ilut.cols() == dim @@ -40,10 +40,10 @@ ilut.factorize(A) assert ilut.info() == eigenpy.ComputationInfo.Success -ilut_params = eigenpy.IncompleteLUT(A, 1e-4, 15) +ilut_params = eigenpy.solvers.IncompleteLUT(A, 1e-4, 15) assert ilut_params.info() == eigenpy.ComputationInfo.Success -ilut_set = eigenpy.IncompleteLUT() +ilut_set = eigenpy.solvers.IncompleteLUT() ilut_set.setDroptol(1e-3) ilut_set.setFillfactor(20) ilut_set.compute(A) diff --git a/unittest/python/test_MINRES.py b/unittest/python/test_MINRES.py index b630ad4bc..9e22fbcf7 100644 --- a/unittest/python/test_MINRES.py +++ b/unittest/python/test_MINRES.py @@ -6,7 +6,7 @@ rng = np.random.default_rng() A = np.eye(dim) -minres = eigenpy.MINRES(A) +minres = eigenpy.solvers.MINRES(A) X = rng.random((dim, 20)) B = A.dot(X) From 98ee63faefcc41289e8498a7c06f527b742f517f Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 21 Jul 2025 00:13:57 +0200 Subject: [PATCH 21/43] Optional arguments with bp::optional --- include/eigenpy/decompositions/BDCSVD.hpp | 23 +++++--------------- include/eigenpy/decompositions/JacobiSVD.hpp | 23 +++++--------------- 2 files changed, 10 insertions(+), 36 deletions(-) diff --git a/include/eigenpy/decompositions/BDCSVD.hpp b/include/eigenpy/decompositions/BDCSVD.hpp index 2b15faf0e..b73f352bd 100644 --- a/include/eigenpy/decompositions/BDCSVD.hpp +++ b/include/eigenpy/decompositions/BDCSVD.hpp @@ -25,26 +25,13 @@ struct BDCSVDVisitor template void visit(PyClass &cl) const { cl.def(bp::init<>(bp::arg("self"), "Default constructor")) - .def(bp::init( - bp::args("self", "rows", "cols"), - "Default Constructor with memory preallocation. ")) - .def(bp::init( + .def(bp::init>( bp::args("self", "rows", "cols", "computationOptions "), - "Default Constructor with memory preallocation. \n\n" - "Like the default constructor but with preallocation of the " - "internal " - "data according to the specified problem size and the " - "computationOptions. ")) - .def(bp::init( - bp::args("self", "matrix"), - "Constructor performing the decomposition of given matrix. ")) - .def(bp::init( + "Default Constructor with memory preallocation. ")) + .def(bp::init>( bp::args("self", "matrix", "computationOptions "), - "Constructor performing the decomposition of given matrix. \n\n" - "One cannot request unitiaries using both the Options template " - "parameter " - "and the constructor. If possible, prefer using the Options " - "template parameter.")) + "Constructor performing the decomposition of given matrix.")) .def("cols", &Solver::cols, bp::arg("self"), "Returns the number of columns. ") diff --git a/include/eigenpy/decompositions/JacobiSVD.hpp b/include/eigenpy/decompositions/JacobiSVD.hpp index b1fd64d7b..ea4a86b80 100644 --- a/include/eigenpy/decompositions/JacobiSVD.hpp +++ b/include/eigenpy/decompositions/JacobiSVD.hpp @@ -24,26 +24,13 @@ struct JacobiSVDVisitor template void visit(PyClass &cl) const { cl.def(bp::init<>(bp::arg("self"), "Default constructor")) - .def(bp::init( - bp::args("self", "rows", "cols"), - "Default Constructor with memory preallocation. ")) - .def(bp::init( + .def(bp::init>( bp::args("self", "rows", "cols", "computationOptions "), - "Default Constructor with memory preallocation. \n\n" - "Like the default constructor but with preallocation of the " - "internal " - "data according to the specified problem size and the " - "computationOptions. ")) - .def(bp::init( - bp::args("self", "matrix"), - "Constructor performing the decomposition of given matrix. ")) - .def(bp::init( + "Default Constructor with memory preallocation.")) + .def(bp::init>( bp::args("self", "matrix", "computationOptions "), - "Constructor performing the decomposition of given matrix. \n\n" - "One cannot request unitiaries using both the Options template " - "parameter " - "and the constructor. If possible, prefer using the Options " - "template parameter.")) + "Constructor performing the decomposition of given matrix.")) .def("cols", &JacobiSVD::cols, bp::arg("self"), "Returns the number of columns. ") From 15fc163991762c0aeb7eb2a90bf499d86158bd30 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 21 Jul 2025 01:17:08 +0200 Subject: [PATCH 22/43] README: Remove the 'SVD and QR to be added', but enounce them --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2552f83fc..c1e9d8763 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ EigenPy — Versatile and efficient Python bindings between Numpy and Eigen - full support Eigen::Ref avoiding memory allocation - full support of the Eigen::Tensor module - exposition of the Geometry module of Eigen for easy code prototyping -- standard matrix decomposion routines of Eigen such as the Cholesky decomposition (SVD and QR decompositions [can be added](#contributing)) +- standard matrix decomposion routines of Eigen such as the Cholesky, SVD and QR decompositions - full support of SWIG objects - full support of runtime declaration of Numpy scalar types - extended API to expose several STL types and some of their Boost equivalents: `optional` types, `std::pair`, maps, variants... From a0ee5d556e3ec75c8ec7ff34618b25de4c936235 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 21 Jul 2025 15:46:11 +0200 Subject: [PATCH 23/43] CI: Fix test_JacobiSVD --- unittest/python/test_JacobiSVD.py | 106 ++++++++++++------------------ 1 file changed, 43 insertions(+), 63 deletions(-) diff --git a/unittest/python/test_JacobiSVD.py b/unittest/python/test_JacobiSVD.py index 0eb5dcacd..db1981277 100644 --- a/unittest/python/test_JacobiSVD.py +++ b/unittest/python/test_JacobiSVD.py @@ -2,20 +2,21 @@ import eigenpy +THIN_U = eigenpy.DecompositionOptions.ComputeThinU +THIN_V = eigenpy.DecompositionOptions.ComputeThinV +FULL_U = eigenpy.DecompositionOptions.ComputeFullU +FULL_V = eigenpy.DecompositionOptions.ComputeFullV + _options = [ 0, - eigenpy.DecompositionOptions.ComputeThinU, - eigenpy.DecompositionOptions.ComputeThinV, - eigenpy.DecompositionOptions.ComputeFullU, - eigenpy.DecompositionOptions.ComputeFullV, - eigenpy.DecompositionOptions.ComputeThinU - | eigenpy.DecompositionOptions.ComputeThinV, - eigenpy.DecompositionOptions.ComputeFullU - | eigenpy.DecompositionOptions.ComputeFullV, - eigenpy.DecompositionOptions.ComputeThinU - | eigenpy.DecompositionOptions.ComputeFullV, - eigenpy.DecompositionOptions.ComputeFullU - | eigenpy.DecompositionOptions.ComputeThinV, + THIN_U, + THIN_V, + FULL_U, + FULL_V, + THIN_U | THIN_V, + FULL_U | FULL_V, + THIN_U | FULL_V, + FULL_U | THIN_V, ] _classes = [ @@ -26,32 +27,30 @@ ] -def test_jacobi(cls, options): +def is_valid_combination(cls, opt): + if cls == eigenpy.FullPivHhJacobiSVD: + has_thin_u = bool(opt & THIN_U) + has_thin_v = bool(opt & THIN_V) + + if has_thin_u or has_thin_v: + return False + + return True + + +def test_jacobi(cls, opt): dim = 100 rng = np.random.default_rng() A = rng.random((dim, dim)) A = (A + A.T) * 0.5 + np.diag(10.0 + rng.random(dim)) - if cls == eigenpy.FullPivHhJacobiSVD: - if options != 0 and not ( - options - & ( - eigenpy.DecompositionOptions.ComputeFullU - | eigenpy.DecompositionOptions.ComputeFullV - ) - ): - return - - jacobisvd = cls(A, options) + jacobisvd = cls(A, opt) assert jacobisvd.info() == eigenpy.ComputationInfo.Success - if options & ( - eigenpy.DecompositionOptions.ComputeThinU - | eigenpy.DecompositionOptions.ComputeFullU - ) and options & ( - eigenpy.DecompositionOptions.ComputeThinV - | eigenpy.DecompositionOptions.ComputeFullV - ): + has_u = opt & (THIN_U | FULL_U) + has_v = opt & (THIN_V | FULL_V) + + if has_u and has_v: X = rng.random((dim, 20)) B = A @ X X_est = jacobisvd.solve(B) @@ -64,13 +63,11 @@ def test_jacobi(cls, options): assert eigenpy.is_approx(x, x_est) assert eigenpy.is_approx(A @ x_est, b) - rows = jacobisvd.rows() - cols = jacobisvd.cols() - assert cols == dim - assert rows == dim + assert jacobisvd.rows() == dim + assert jacobisvd.cols() == dim _jacobisvd_compute = jacobisvd.compute(A) - _jacobisvd_compute_options = jacobisvd.compute(A, options) + _jacobisvd_compute_options = jacobisvd.compute(A, opt) rank = jacobisvd.rank() singularvalues = jacobisvd.singularValues() @@ -84,37 +81,19 @@ def test_jacobi(cls, options): compute_u = jacobisvd.computeU() compute_v = jacobisvd.computeV() - expected_compute_u = bool( - options - & ( - eigenpy.DecompositionOptions.ComputeThinU - | eigenpy.DecompositionOptions.ComputeFullU - ) - ) - expected_compute_v = bool( - options - & ( - eigenpy.DecompositionOptions.ComputeThinV - | eigenpy.DecompositionOptions.ComputeFullV - ) - ) + expected_compute_u = bool(has_u) + expected_compute_v = bool(has_v) assert compute_u == expected_compute_u assert compute_v == expected_compute_v if compute_u: matrixU = jacobisvd.matrixU() - if options & eigenpy.DecompositionOptions.ComputeFullU: - assert matrixU.shape == (dim, dim) - elif options & eigenpy.DecompositionOptions.ComputeThinU: - assert matrixU.shape == (dim, dim) + assert matrixU.shape == (dim, dim) assert eigenpy.is_approx(matrixU.T @ matrixU, np.eye(matrixU.shape[1])) if compute_v: matrixV = jacobisvd.matrixV() - if options & eigenpy.DecompositionOptions.ComputeFullV: - assert matrixV.shape == (dim, dim) - elif options & eigenpy.DecompositionOptions.ComputeThinV: - assert matrixV.shape == (dim, dim) + assert matrixV.shape == (dim, dim) assert eigenpy.is_approx(matrixV.T @ matrixV, np.eye(matrixV.shape[1])) if compute_u and compute_v: @@ -138,8 +117,8 @@ def test_jacobi(cls, options): assert id1 == decomp1.id() assert id2 == decomp2.id() - decomp3 = cls(dim, dim, options) - decomp4 = cls(dim, dim, options) + decomp3 = cls(dim, dim, opt) + decomp4 = cls(dim, dim, opt) id3 = decomp3.id() id4 = decomp4.id() assert id3 != id4 @@ -147,6 +126,7 @@ def test_jacobi(cls, options): assert id4 == decomp4.id() -for opt in _options: - for cls in _classes: - test_jacobi(cls, opt) +for cls in _classes: + for opt in _options: + if is_valid_combination(cls, opt): + test_jacobi(cls, opt) From 5cc2cbc3710edc78f2ad24136209ff4351b4e53d Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 21 Jul 2025 19:41:30 +0200 Subject: [PATCH 24/43] is_approx: Put is_approx and set the precision --- .../decompositions/SelfAdjointEigenSolver.hpp | 17 ++++++++----- unittest/python/test_ComplexSchur.py | 24 +++++++++---------- .../python/test_GeneralizedEigenSolver.py | 8 +++---- .../test_GeneralizedSelfAdjointEigenSolver.py | 8 +++---- .../python/test_HessenbergDecomposition.py | 8 +++---- unittest/python/test_RealQZ.py | 12 +++++----- unittest/python/test_RealSchur.py | 22 ++++++++--------- unittest/python/test_Tridiagonalization.py | 14 +++++------ 8 files changed, 58 insertions(+), 55 deletions(-) diff --git a/include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp b/include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp index 221f3d434..6d1bf6c32 100644 --- a/include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp +++ b/include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp @@ -21,6 +21,7 @@ struct SelfAdjointEigenSolverVisitor typedef _MatrixType MatrixType; typedef typename MatrixType::Scalar Scalar; typedef Eigen::SelfAdjointEigenSolver Solver; + typedef Eigen::Matrix VectorType; template void visit(PyClass& cl) const { @@ -32,12 +33,16 @@ struct SelfAdjointEigenSolverVisitor bp::args("self", "matrix", "options"), "Computes eigendecomposition of given matrix")) - .def("eigenvalues", &Solver::eigenvalues, bp::arg("self"), - "Returns the eigenvalues of given matrix.", - bp::return_internal_reference<>()) - .def("eigenvectors", &Solver::eigenvectors, bp::arg("self"), - "Returns the eigenvectors of given matrix.", - bp::return_internal_reference<>()) + .def( + "eigenvalues", + +[](Solver& c) -> const VectorType& { return c.eigenvalues(); }, + "Returns the eigenvalues of given matrix.", + bp::return_internal_reference<>()) + .def( + "eigenvectors", + +[](Solver& c) -> const MatrixType& { return c.eigenvectors(); }, + "Returns the eigenvectors of given matrix.", + bp::return_internal_reference<>()) .def("compute", &SelfAdjointEigenSolverVisitor::compute_proxy, diff --git a/unittest/python/test_ComplexSchur.py b/unittest/python/test_ComplexSchur.py index fe0503307..2370a4e22 100644 --- a/unittest/python/test_ComplexSchur.py +++ b/unittest/python/test_ComplexSchur.py @@ -13,8 +13,8 @@ T = cs.matrixT() A_complex = A.astype(complex) -assert eigenpy.is_approx(A_complex, U @ T @ U.conj().T, 1e-10) -assert eigenpy.is_approx(U @ U.conj().T, np.eye(dim), 1e-10) +assert eigenpy.is_approx(A_complex, U @ T @ U.conj().T) +assert eigenpy.is_approx(U @ U.conj().T, np.eye(dim)) for row in range(1, dim): for col in range(row): @@ -33,20 +33,20 @@ T2 = cs2.matrixT() U2 = cs2.matrixU() -assert eigenpy.is_approx(T1, T2, 1e-12) -assert eigenpy.is_approx(U1, U2, 1e-12) +assert eigenpy.is_approx(T1, T2) +assert eigenpy.is_approx(U1, U2) cs_no_u = eigenpy.ComplexSchur(A, False) assert cs_no_u.info() == eigenpy.ComputationInfo.Success T_no_u = cs_no_u.matrixT() -assert eigenpy.is_approx(T, T_no_u, 1e-12) +assert eigenpy.is_approx(T, T_no_u) cs_compute_no_u = eigenpy.ComplexSchur(dim) result_no_u = cs_compute_no_u.compute(A, False) assert result_no_u.info() == eigenpy.ComputationInfo.Success T_compute_no_u = cs_compute_no_u.matrixT() -assert eigenpy.is_approx(T, T_compute_no_u, 1e-12) +assert eigenpy.is_approx(T, T_compute_no_u) cs_iter = eigenpy.ComplexSchur(dim) cs_iter.setMaxIterations(30 * dim) # m_maxIterationsPerRow * size @@ -56,8 +56,8 @@ T_iter = cs_iter.matrixT() U_iter = cs_iter.matrixU() -assert eigenpy.is_approx(T, T_iter, 1e-12) -assert eigenpy.is_approx(U, U_iter, 1e-12) +assert eigenpy.is_approx(T, T_iter) +assert eigenpy.is_approx(U, U_iter) cs_few_iter = eigenpy.ComplexSchur(dim) cs_few_iter.setMaxIterations(1) @@ -74,8 +74,8 @@ U_triangular = cs_triangular.matrixU() A_triangular_complex = A_triangular.astype(complex) -assert eigenpy.is_approx(T_triangular, A_triangular_complex, 1e-10) -assert eigenpy.is_approx(U_triangular, np.eye(dim, dtype=complex), 1e-10) +assert eigenpy.is_approx(T_triangular, A_triangular_complex) +assert eigenpy.is_approx(U_triangular, np.eye(dim, dtype=complex)) hess = eigenpy.HessenbergDecomposition(A) H = hess.matrixH() @@ -89,9 +89,7 @@ U_from_hess = cs_from_hess.matrixU() A_complex = A.astype(complex) -assert eigenpy.is_approx( - A_complex, U_from_hess @ T_from_hess @ U_from_hess.conj().T, 1e-10 -) +assert eigenpy.is_approx(A_complex, U_from_hess @ T_from_hess @ U_from_hess.conj().T) cs1_id = eigenpy.ComplexSchur(dim) cs2_id = eigenpy.ComplexSchur(dim) diff --git a/unittest/python/test_GeneralizedEigenSolver.py b/unittest/python/test_GeneralizedEigenSolver.py index db1dbdb76..89e2ba416 100644 --- a/unittest/python/test_GeneralizedEigenSolver.py +++ b/unittest/python/test_GeneralizedEigenSolver.py @@ -35,8 +35,8 @@ Av = A @ v lambda_Bv = lambda_k * (B @ v) - assert eigenpy.is_approx(Av.real, lambda_Bv.real, 1e-10) - assert eigenpy.is_approx(Av.imag, lambda_Bv.imag, 1e-10) + assert eigenpy.is_approx(Av.real, lambda_Bv.real, 1e-6) + assert eigenpy.is_approx(Av.imag, lambda_Bv.imag, 1e-6) for k in range(dim): v = eigenvectors[:, k] @@ -45,8 +45,8 @@ alpha_Bv = alpha * (B @ v) beta_Av = beta * (A @ v) - assert eigenpy.is_approx(alpha_Bv.real, beta_Av.real, 1e-10) - assert eigenpy.is_approx(alpha_Bv.imag, beta_Av.imag, 1e-10) + assert eigenpy.is_approx(alpha_Bv.real, beta_Av.real, 1e-6) + assert eigenpy.is_approx(alpha_Bv.imag, beta_Av.imag, 1e-6) for k in range(dim): if abs(betas[k]) > 1e-12: diff --git a/unittest/python/test_GeneralizedSelfAdjointEigenSolver.py b/unittest/python/test_GeneralizedSelfAdjointEigenSolver.py index a6b95a630..e42c221f9 100644 --- a/unittest/python/test_GeneralizedSelfAdjointEigenSolver.py +++ b/unittest/python/test_GeneralizedSelfAdjointEigenSolver.py @@ -45,10 +45,10 @@ def test_generalized_selfadjoint_eigensolver(options): lam = D[i] Av = A @ v lam_Bv = lam * (B @ v) - assert eigenpy.is_approx(Av, lam_Bv, 1e-10) + assert eigenpy.is_approx(Av, lam_Bv, 1e-6) VT_B_V = V.T @ B @ V - assert eigenpy.is_approx(VT_B_V, np.eye(dim), 1e-10) + assert eigenpy.is_approx(VT_B_V, np.eye(dim), 1e-6) elif options & eigenpy.DecompositionOptions.ABx_lx: AB = A @ B @@ -57,7 +57,7 @@ def test_generalized_selfadjoint_eigensolver(options): lam = D[i] ABv = AB @ v lam_v = lam * v - assert np.allclose(ABv, lam_v) + assert eigenpy.is_approx(ABv, lam_v, 1e-6) elif options & eigenpy.DecompositionOptions.BAx_lx: BA = B @ A @@ -66,7 +66,7 @@ def test_generalized_selfadjoint_eigensolver(options): lam = D[i] BAv = BA @ v lam_v = lam * v - assert np.allclose(BAv, lam_v) + assert eigenpy.is_approx(BAv, lam_v, 1e-6) _gsaes_compute = gsaes.compute(A, B) _gsaes_compute_options = gsaes.compute(A, B, options) diff --git a/unittest/python/test_HessenbergDecomposition.py b/unittest/python/test_HessenbergDecomposition.py index 449842fb9..095ac7105 100644 --- a/unittest/python/test_HessenbergDecomposition.py +++ b/unittest/python/test_HessenbergDecomposition.py @@ -15,7 +15,7 @@ A_reconstructed = Q @ H @ Q.conj().T else: A_reconstructed = Q @ H @ Q.T -assert eigenpy.is_approx(A, A_reconstructed, 1e-10) +assert eigenpy.is_approx(A, A_reconstructed) for row in range(2, dim): for col in range(row - 1): @@ -25,7 +25,7 @@ QQ_conj = Q @ Q.conj().T else: QQ_conj = Q @ Q.T -assert eigenpy.is_approx(QQ_conj, np.eye(dim), 1e-10) +assert eigenpy.is_approx(QQ_conj, np.eye(dim)) A_test = rng.random((dim, dim)) hess1 = eigenpy.HessenbergDecomposition(dim) @@ -37,8 +37,8 @@ Q1 = hess1.matrixQ() Q2 = hess2.matrixQ() -assert eigenpy.is_approx(H1, H2, 1e-12) -assert eigenpy.is_approx(Q1, Q2, 1e-12) +assert eigenpy.is_approx(H1, H2) +assert eigenpy.is_approx(Q1, Q2) hCoeffs = hess.householderCoefficients() packed = hess.packedMatrix() diff --git a/unittest/python/test_RealQZ.py b/unittest/python/test_RealQZ.py index 9f5f48d1a..30411c408 100644 --- a/unittest/python/test_RealQZ.py +++ b/unittest/python/test_RealQZ.py @@ -15,11 +15,11 @@ Z = realqz.matrixZ() T = realqz.matrixT() -assert eigenpy.is_approx(A, Q @ S @ Z, 1e-10) -assert eigenpy.is_approx(B, Q @ T @ Z, 1e-10) +assert eigenpy.is_approx(A, Q @ S @ Z) +assert eigenpy.is_approx(B, Q @ T @ Z) -assert eigenpy.is_approx(Q @ Q.T, np.eye(dim), 1e-10) -assert eigenpy.is_approx(Z @ Z.T, np.eye(dim), 1e-10) +assert eigenpy.is_approx(Q @ Q.T, np.eye(dim)) +assert eigenpy.is_approx(Z @ Z.T, np.eye(dim)) for i in range(dim): for j in range(i): @@ -62,8 +62,8 @@ S_without = realqz_without_qz.matrixS() T_without = realqz_without_qz.matrixT() -assert eigenpy.is_approx(S_with, S_without, 1e-12) -assert eigenpy.is_approx(T_with, T_without, 1e-12) +assert eigenpy.is_approx(S_with, S_without) +assert eigenpy.is_approx(T_with, T_without) iterations = realqz.iterations() assert iterations >= 0 diff --git a/unittest/python/test_RealSchur.py b/unittest/python/test_RealSchur.py index 788d0efc3..78552a286 100644 --- a/unittest/python/test_RealSchur.py +++ b/unittest/python/test_RealSchur.py @@ -30,8 +30,8 @@ def verify_is_quasi_triangular(T): U = rs.matrixU() T = rs.matrixT() -assert eigenpy.is_approx(A, U @ T @ U.T, 1e-10) -assert eigenpy.is_approx(U @ U.T, np.eye(dim), 1e-10) +assert eigenpy.is_approx(A, U @ T @ U.T) +assert eigenpy.is_approx(U @ U.T, np.eye(dim)) verify_is_quasi_triangular(T) @@ -48,20 +48,20 @@ def verify_is_quasi_triangular(T): T2 = rs2.matrixT() U2 = rs2.matrixU() -assert eigenpy.is_approx(T1, T2, 1e-12) -assert eigenpy.is_approx(U1, U2, 1e-12) +assert eigenpy.is_approx(T1, T2) +assert eigenpy.is_approx(U1, U2) rs_no_u = eigenpy.RealSchur(A, False) assert rs_no_u.info() == eigenpy.ComputationInfo.Success T_no_u = rs_no_u.matrixT() -assert eigenpy.is_approx(T, T_no_u, 1e-12) +assert eigenpy.is_approx(T, T_no_u) rs_compute_no_u = eigenpy.RealSchur(dim) result_no_u = rs_compute_no_u.compute(A, False) assert result_no_u.info() == eigenpy.ComputationInfo.Success T_compute_no_u = rs_compute_no_u.matrixT() -assert eigenpy.is_approx(T, T_compute_no_u, 1e-12) +assert eigenpy.is_approx(T, T_compute_no_u) rs_iter = eigenpy.RealSchur(dim) rs_iter.setMaxIterations(40 * dim) # m_maxIterationsPerRow * size @@ -71,8 +71,8 @@ def verify_is_quasi_triangular(T): T_iter = rs_iter.matrixT() U_iter = rs_iter.matrixU() -assert eigenpy.is_approx(T, T_iter, 1e-12) -assert eigenpy.is_approx(U, U_iter, 1e-12) +assert eigenpy.is_approx(T, T_iter) +assert eigenpy.is_approx(U, U_iter) if dim > 2: rs_few_iter = eigenpy.RealSchur(dim) @@ -89,8 +89,8 @@ def verify_is_quasi_triangular(T): T_triangular = rs_triangular.matrixT() U_triangular = rs_triangular.matrixU() -assert eigenpy.is_approx(T_triangular, A_triangular, 1e-10) -assert eigenpy.is_approx(U_triangular, np.eye(dim), 1e-10) +assert eigenpy.is_approx(T_triangular, A_triangular) +assert eigenpy.is_approx(U_triangular, np.eye(dim)) hess = eigenpy.HessenbergDecomposition(A) H = hess.matrixH() @@ -103,7 +103,7 @@ def verify_is_quasi_triangular(T): T_from_hess = rs_from_hess.matrixT() U_from_hess = rs_from_hess.matrixU() -assert eigenpy.is_approx(A, U_from_hess @ T_from_hess @ U_from_hess.T, 1e-10) +assert eigenpy.is_approx(A, U_from_hess @ T_from_hess @ U_from_hess.T) rs1_id = eigenpy.RealSchur(dim) rs2_id = eigenpy.RealSchur(dim) diff --git a/unittest/python/test_Tridiagonalization.py b/unittest/python/test_Tridiagonalization.py index 70b43d119..1270c778d 100644 --- a/unittest/python/test_Tridiagonalization.py +++ b/unittest/python/test_Tridiagonalization.py @@ -12,15 +12,15 @@ Q = tri.matrixQ() T = tri.matrixT() -assert eigenpy.is_approx(A, Q @ T @ Q.T, 1e-10) -assert eigenpy.is_approx(Q @ Q.T, np.eye(dim), 1e-10) +assert eigenpy.is_approx(A, Q @ T @ Q.T) +assert eigenpy.is_approx(Q @ Q.T, np.eye(dim)) for i in range(dim): for j in range(dim): if abs(i - j) > 1: assert abs(T[i, j]) < 1e-12 -assert eigenpy.is_approx(T, T.T, 1e-12) +assert eigenpy.is_approx(T, T.T) diag = tri.diagonal() sub_diag = tri.subDiagonal() @@ -43,8 +43,8 @@ Q2 = tri2.matrixQ() T2 = tri2.matrixT() -assert eigenpy.is_approx(Q1, Q2, 1e-12) -assert eigenpy.is_approx(T1, T2, 1e-12) +assert eigenpy.is_approx(Q1, Q2) +assert eigenpy.is_approx(T1, T2) h_coeffs = tri.householderCoefficients() packed = tri.packedMatrix() @@ -66,7 +66,7 @@ Q_diag = tri_diag.matrixQ() T_diag = tri_diag.matrixT() -assert eigenpy.is_approx(A_diag, Q_diag @ T_diag @ Q_diag.T, 1e-10) +assert eigenpy.is_approx(A_diag, Q_diag @ T_diag @ Q_diag.T) for i in range(dim): for j in range(dim): if i != j: @@ -84,7 +84,7 @@ Q_tridiag = tri_tridiag.matrixQ() T_tridiag = tri_tridiag.matrixT() -assert eigenpy.is_approx(A_tridiag, Q_tridiag @ T_tridiag @ Q_tridiag.T, 1e-10) +assert eigenpy.is_approx(A_tridiag, Q_tridiag @ T_tridiag @ Q_tridiag.T) tri1_id = eigenpy.Tridiagonalization(dim) From 7abc77b3fdba35a5a9c6bef81a66fd157c9a8ed8 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Tue, 22 Jul 2025 13:33:56 +0200 Subject: [PATCH 25/43] Add const in some lambda functions on Solver& to stick to definition --- .../decompositions/SelfAdjointEigenSolver.hpp | 8 ++++++-- .../decompositions/Tridiagonalization.hpp | 3 ++- include/eigenpy/solvers/IncompleteCholesky.hpp | 16 ++++++++++------ include/eigenpy/solvers/IncompleteLUT.hpp | 6 +++--- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp b/include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp index 6d1bf6c32..987577d6f 100644 --- a/include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp +++ b/include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp @@ -35,12 +35,16 @@ struct SelfAdjointEigenSolverVisitor .def( "eigenvalues", - +[](Solver& c) -> const VectorType& { return c.eigenvalues(); }, + +[](const Solver& c) -> const VectorType& { + return c.eigenvalues(); + }, "Returns the eigenvalues of given matrix.", bp::return_internal_reference<>()) .def( "eigenvectors", - +[](Solver& c) -> const MatrixType& { return c.eigenvectors(); }, + +[](const Solver& c) -> const MatrixType& { + return c.eigenvectors(); + }, "Returns the eigenvectors of given matrix.", bp::return_internal_reference<>()) diff --git a/include/eigenpy/decompositions/Tridiagonalization.hpp b/include/eigenpy/decompositions/Tridiagonalization.hpp index d3cd96f56..5b8b78f17 100644 --- a/include/eigenpy/decompositions/Tridiagonalization.hpp +++ b/include/eigenpy/decompositions/Tridiagonalization.hpp @@ -49,7 +49,8 @@ struct TridiagonalizationVisitor : public boost::python::def_visitor< +[](const Solver &c) -> MatrixType { return c.matrixQ(); }, "Returns the unitary matrix Q in the decomposition.") .def( - "matrixT", +[](Solver &c) -> MatrixType { return c.matrixT(); }, + "matrixT", + +[](const Solver &c) -> MatrixType { return c.matrixT(); }, "Returns an expression of the tridiagonal matrix T in the " "decomposition.") diff --git a/include/eigenpy/solvers/IncompleteCholesky.hpp b/include/eigenpy/solvers/IncompleteCholesky.hpp index 0786035b3..302c6f78c 100644 --- a/include/eigenpy/solvers/IncompleteCholesky.hpp +++ b/include/eigenpy/solvers/IncompleteCholesky.hpp @@ -63,36 +63,40 @@ struct IncompleteCholeskyVisitor : public boost::python::def_visitor< .def( "matrixL", - +[](Solver& self) -> const FactorType& { return self.matrixL(); }, + +[](const Solver& self) -> const FactorType& { + return self.matrixL(); + }, bp::return_value_policy()) .def( "scalingS", - +[](Solver& self) -> const VectorRx& { return self.scalingS(); }, + +[](const Solver& self) -> const VectorRx& { + return self.scalingS(); + }, bp::return_value_policy()) .def( "permutationP", - +[](Solver& self) -> const PermutationType& { + +[](const Solver& self) -> const PermutationType& { return self.permutationP(); }, bp::return_value_policy()) .def( "solve", - +[](Solver const& self, const Eigen::Ref& b) + +[](const Solver& self, const Eigen::Ref& b) -> DenseVectorXs { return self.solve(b); }, bp::arg("b"), "Returns the solution x of A x = b using the current decomposition " "of A, where b is a right hand side vector.") .def( "solve", - +[](Solver const& self, const Eigen::Ref& B) + +[](const Solver& self, const Eigen::Ref& B) -> DenseMatrixXs { return self.solve(B); }, bp::arg("b"), "Returns the solution X of A X = B using the current decomposition " "of A where B is a right hand side matrix.") .def( "solve", - +[](Solver const& self, const MatrixType& B) -> MatrixType { + +[](const Solver& self, const MatrixType& B) -> MatrixType { DenseMatrixXs B_dense = DenseMatrixXs(B); DenseMatrixXs X_dense = self.solve(B_dense); return MatrixType(X_dense.sparseView()); diff --git a/include/eigenpy/solvers/IncompleteLUT.hpp b/include/eigenpy/solvers/IncompleteLUT.hpp index 5ea730205..43edb18c0 100644 --- a/include/eigenpy/solvers/IncompleteLUT.hpp +++ b/include/eigenpy/solvers/IncompleteLUT.hpp @@ -63,21 +63,21 @@ struct IncompleteLUTVisitor .def( "solve", - +[](Solver const& self, const Eigen::Ref& b) + +[](const Solver& self, const Eigen::Ref& b) -> DenseVectorXs { return self.solve(b); }, bp::arg("b"), "Returns the solution x of A x = b using the current decomposition " "of A, where b is a right hand side vector.") .def( "solve", - +[](Solver const& self, const Eigen::Ref& B) + +[](const Solver& self, const Eigen::Ref& B) -> DenseMatrixXs { return self.solve(B); }, bp::arg("b"), "Returns the solution X of A X = B using the current decomposition " "of A where B is a right hand side matrix.") .def( "solve", - +[](Solver const& self, const MatrixType& B) -> MatrixType { + +[](const Solver& self, const MatrixType& B) -> MatrixType { DenseMatrixXs B_dense = DenseMatrixXs(B); DenseMatrixXs X_dense = self.solve(B_dense); return MatrixType(X_dense.sparseView()); From 43be3a674c18369939cd8dec8f7557ac4993939b Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Wed, 23 Jul 2025 14:11:04 +0200 Subject: [PATCH 26/43] Add matrixQ in SparseQR --- include/eigenpy/decompositions/JacobiSVD.hpp | 2 +- include/eigenpy/decompositions/sparse/QR.hpp | 163 +++++++++++++++--- src/decompositions/sparse-qr-solver.cpp | 10 +- .../decompositions/sparse/test_SparseQR.py | 36 ++++ 4 files changed, 182 insertions(+), 29 deletions(-) diff --git a/include/eigenpy/decompositions/JacobiSVD.hpp b/include/eigenpy/decompositions/JacobiSVD.hpp index ea4a86b80..130b35a50 100644 --- a/include/eigenpy/decompositions/JacobiSVD.hpp +++ b/include/eigenpy/decompositions/JacobiSVD.hpp @@ -52,7 +52,7 @@ struct JacobiSVDVisitor "specified by the computationOptions parameter. ", bp::return_self<>()) .def("rows", &JacobiSVD::rows, bp::arg("self"), - "Returns the number of rows. . ") + "Returns the number of rows.") .def(SVDBaseVisitor()); } diff --git a/include/eigenpy/decompositions/sparse/QR.hpp b/include/eigenpy/decompositions/sparse/QR.hpp index ea64117ae..d0b26b12d 100644 --- a/include/eigenpy/decompositions/sparse/QR.hpp +++ b/include/eigenpy/decompositions/sparse/QR.hpp @@ -14,16 +14,108 @@ namespace eigenpy { -template > -struct SparseQRVisitor : public boost::python::def_visitor< - SparseQRVisitor<_MatrixType, _Ordering>> { - typedef SparseQRVisitor<_MatrixType, _Ordering> Visitor; - typedef _MatrixType MatrixType; - typedef _Ordering Ordering; - - typedef Eigen::SparseQR Solver; +template +struct SparseQRMatrixQTransposeReturnTypeVisitor + : public boost::python::def_visitor< + SparseQRMatrixQTransposeReturnTypeVisitor> { + typedef typename SparseQRType::Scalar Scalar; + typedef Eigen::SparseQRMatrixQTransposeReturnType + QTransposeType; + typedef Eigen::VectorXd VectorXd; + typedef Eigen::MatrixXd MatrixXd; + + template + void visit(PyClass& cl) const { + cl.def(bp::init(bp::args("self", "qr"))) + .def( + "__matmul__", + +[](QTransposeType& self, const MatrixXd& other) -> MatrixXd { + return MatrixXd(self * other); + }, + bp::args("self", "other")) + + .def( + "__matmul__", + +[](QTransposeType& self, const VectorXd& other) -> VectorXd { + return VectorXd(self * other); + }, + bp::args("self", "other")); + } + + static void expose() { + static const std::string classname = "SparseQRMatrixQTransposeReturnType_" + + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string& name) { + bp::class_( + name.c_str(), "Eigen SparseQRMatrixQTransposeReturnType", bp::no_init) + .def(SparseQRMatrixQTransposeReturnTypeVisitor()) + .def(IdVisitor()); + } +}; + +template +struct SparseQRMatrixQReturnTypeVisitor + : public boost::python::def_visitor< + SparseQRMatrixQReturnTypeVisitor> { + typedef typename SparseQRType::Scalar Scalar; + typedef Eigen::SparseQRMatrixQTransposeReturnType + QTransposeType; + typedef Eigen::SparseQRMatrixQReturnType QType; + typedef Eigen::VectorXd VectorXd; + typedef Eigen::MatrixXd MatrixXd; + + template + void visit(PyClass& cl) const { + cl.def(bp::init(bp::args("self", "qr"))) + .def( + "__matmul__", + +[](QType& self, const MatrixXd& other) -> MatrixXd { + return MatrixXd(self * other); + }, + bp::args("self", "other")) + + .def( + "__matmul__", + +[](QType& self, const VectorXd& other) -> VectorXd { + return VectorXd(self * other); + }, + bp::args("self", "other")) + + .def("rows", &QType::rows) + .def("cols", &QType::cols) + + .def( + "adjoint", + +[](const QType& self) -> QTransposeType { return self.adjoint(); }) + + .def( + "transpose", +[](const QType& self) -> QTransposeType { + return self.transpose(); + }); + } + + static void expose() { + static const std::string classname = + "SparseQRMatrixQReturnType_" + scalar_name::shortname(); + expose(classname); + } + + static void expose(const std::string& name) { + bp::class_(name.c_str(), "Eigen SparseQRMatrixQReturnType", + bp::no_init) + .def(SparseQRMatrixQReturnTypeVisitor()) + .def(IdVisitor()); + } +}; + +template +struct SparseQRVisitor + : public boost::python::def_visitor> { + typedef typename SparseQRType::MatrixType MatrixType; + typedef typename MatrixType::Scalar Scalar; typedef typename MatrixType::RealScalar RealScalar; typedef Eigen::Matrix @@ -32,53 +124,70 @@ struct SparseQRVisitor : public boost::python::def_visitor< MatrixType::Options> DenseMatrixXs; - typedef Eigen::SparseQRMatrixQReturnType MatrixQType; + typedef typename SparseQRType::QRMatrixType QRMatrixType; + typedef Eigen::SparseQRMatrixQReturnType QType; template - void visit(PyClass &cl) const { + void visit(PyClass& cl) const { cl.def(bp::init<>(bp::arg("self"), "Default constructor")) .def(bp::init( bp::args("self", "mat"), "Construct a QR factorization of the matrix mat.")) - .def("cols", &Solver::cols, bp::arg("self"), + .def("cols", &SparseQRType::cols, bp::arg("self"), "Returns the number of columns of the represented matrix. ") - .def("rows", &Solver::rows, bp::arg("self"), + .def("rows", &SparseQRType::rows, bp::arg("self"), "Returns the number of rows of the represented matrix. ") - .def("compute", &Solver::compute, bp::args("self", "matrix"), + .def("compute", &SparseQRType::compute, bp::args("self", "matrix"), "Compute the symbolic and numeric factorization of the input " "sparse matrix. " "The input matrix should be in column-major storage. ") - .def("analyzePattern", &Solver::analyzePattern, bp::args("self", "mat"), + .def("analyzePattern", &SparseQRType::analyzePattern, + bp::args("self", "mat"), "Compute the column permutation to minimize the fill-in.") - .def("factorize", &Solver::factorize, bp::args("self", "matrix"), + .def("factorize", &SparseQRType::factorize, bp::args("self", "matrix"), "Performs a numeric decomposition of a given matrix.\n" "The given matrix must has the same sparcity than the matrix on " "which the symbolic decomposition has been performed.") - .def("colsPermutation", &Solver::colsPermutation, bp::arg("self"), + .def("colsPermutation", &SparseQRType::colsPermutation, bp::arg("self"), "Returns a reference to the column matrix permutation PTc such " "that Pr A PTc = LU.", bp::return_value_policy()) - .def("info", &Solver::info, bp::arg("self"), + .def("info", &SparseQRType::info, bp::arg("self"), "NumericalIssue if the input contains INF or NaN values or " "overflow occured. Returns Success otherwise.") - .def("lastErrorMessage", &Solver::lastErrorMessage, bp::arg("self"), - "Returns a string describing the type of error. ") + .def("lastErrorMessage", &SparseQRType::lastErrorMessage, + bp::arg("self"), "Returns a string describing the type of error. ") - .def("rank", &Solver::rank, bp::arg("self"), + .def("rank", &SparseQRType::rank, bp::arg("self"), "Returns the number of non linearly dependent columns as " "determined " "by the pivoting threshold. ") - .def("setPivotThreshold", &Solver::setPivotThreshold, + .def( + "matrixQ", + +[](const SparseQRType& self) -> QType { return self.matrixQ(); }, + "Returns an expression of the matrix Q as products of sparse " + "Householder reflectors.") + .def( + "matrixR", + +[](const SparseQRType& self) -> const QRMatrixType& { + return self.matrixR(); + }, + "Returns a const reference to the \b sparse upper triangular " + "matrix " + "R of the QR factorization.", + bp::return_value_policy()) + + .def("setPivotThreshold", &SparseQRType::setPivotThreshold, bp::args("self", "thresh"), "Set the threshold used for a diagonal entry to be an acceptable " "pivot.") - .def(SparseSolverBaseVisitor()); + .def(SparseSolverBaseVisitor()); } static void expose() { @@ -87,8 +196,8 @@ struct SparseQRVisitor : public boost::python::def_visitor< expose(classname); } - static void expose(const std::string &name) { - bp::class_( + static void expose(const std::string& name) { + bp::class_( name.c_str(), "Sparse left-looking QR factorization with numerical column pivoting. " "This class implements a left-looking QR decomposition of sparse " @@ -108,7 +217,7 @@ struct SparseQRVisitor : public boost::python::def_visitor< "when A is rank-deficient. \n\n", bp::no_init) .def(SparseQRVisitor()) - .def(IdVisitor()); + .def(IdVisitor()); } }; diff --git a/src/decompositions/sparse-qr-solver.cpp b/src/decompositions/sparse-qr-solver.cpp index 7e00d36f4..1460251bd 100644 --- a/src/decompositions/sparse-qr-solver.cpp +++ b/src/decompositions/sparse-qr-solver.cpp @@ -7,7 +7,15 @@ namespace eigenpy { void exposeSparseQRSolver() { using namespace Eigen; + typedef SparseMatrix ColMajorSparseMatrix; - SparseQRVisitor::expose("SparseQR"); + typedef COLAMDOrdering Ordering; + typedef SparseQR SparseQRType; + + SparseQRMatrixQTransposeReturnTypeVisitor::expose( + "SparseQRMatrixQTransposeReturnType"); + SparseQRMatrixQReturnTypeVisitor::expose( + "SparseQRMatrixQReturnType"); + SparseQRVisitor::expose("SparseQR"); } } // namespace eigenpy diff --git a/unittest/python/decompositions/sparse/test_SparseQR.py b/unittest/python/decompositions/sparse/test_SparseQR.py index 0d37179e3..6f86dbfe3 100644 --- a/unittest/python/decompositions/sparse/test_SparseQR.py +++ b/unittest/python/decompositions/sparse/test_SparseQR.py @@ -36,3 +36,39 @@ assert isinstance(X_est, spa.csc_matrix) assert eigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) assert eigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) + +Q = spqr.matrixQ() +R = spqr.matrixR() +P = spqr.colsPermutation() + +assert spqr.matrixQ().rows() == dim +assert spqr.matrixQ().cols() == dim +assert R.shape[0] == dim +assert R.shape[1] == dim +assert P.indices().size == dim + +test_vec = rng.random(dim) +test_matrix = rng.random((dim, 20)) + +Qv = Q @ test_vec +QM = Q @ test_matrix +Qt = Q.transpose() +QtV = Qt @ test_vec +QtM = Qt @ test_matrix + +assert Qv.shape == (dim,) +assert QM.shape == (dim, 20) +assert QtV.shape == (dim,) +assert QtM.shape == (dim, 20) + +Qa_real_mat = Q.adjoint() +QaV = Qa_real_mat @ test_vec +assert eigenpy.is_approx(QtV, QaV) + +A_dense = A.toarray() +P_indices = np.array([P.indices()[i] for i in range(dim)]) +A_permuted = A_dense[:, P_indices] + +QtAP = Qt @ A_permuted +R_dense = spqr.matrixR().toarray() +assert eigenpy.is_approx(QtAP, R_dense) From 2c5d6459d5f258ad0cd05f43749cbe6404b5aa66 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Wed, 23 Jul 2025 14:18:50 +0200 Subject: [PATCH 27/43] Add actual date in copyrights --- include/eigenpy/decompositions/BDCSVD.hpp | 2 +- include/eigenpy/decompositions/ComplexEigenSolver.hpp | 2 +- include/eigenpy/decompositions/ComplexSchur.hpp | 2 +- include/eigenpy/decompositions/FullPivLU.hpp | 2 +- include/eigenpy/decompositions/GeneralizedEigenSolver.hpp | 2 +- .../decompositions/GeneralizedSelfAdjointEigenSolver.hpp | 2 +- include/eigenpy/decompositions/HessenbergDecomposition.hpp | 2 +- include/eigenpy/decompositions/JacobiSVD.hpp | 2 +- include/eigenpy/decompositions/PartialPivLU.hpp | 2 +- include/eigenpy/decompositions/RealQZ.hpp | 2 +- include/eigenpy/decompositions/RealSchur.hpp | 2 +- include/eigenpy/decompositions/SVDBase.hpp | 2 +- include/eigenpy/decompositions/Tridiagonalization.hpp | 2 +- include/eigenpy/decompositions/sparse/LU.hpp | 2 +- include/eigenpy/decompositions/sparse/QR.hpp | 2 +- include/eigenpy/solvers/BiCGSTAB.hpp | 2 +- include/eigenpy/solvers/IncompleteCholesky.hpp | 2 +- include/eigenpy/solvers/IncompleteLUT.hpp | 2 +- src/decompositions/bdcsvd-solver.cpp | 2 +- src/decompositions/complex-eigen-solver.cpp | 2 +- src/decompositions/complex-schur.cpp | 2 +- src/decompositions/generalized-eigen-solver.cpp | 2 +- src/decompositions/generalized-self-adjoint-eigen-solver.cpp | 2 +- src/decompositions/hessenberg-decomposition.cpp | 2 +- src/decompositions/jacobisvd-solver.cpp | 2 +- src/decompositions/real-qz.cpp | 2 +- src/decompositions/real-schur.cpp | 2 +- src/decompositions/sparse-lu-solver.cpp | 2 +- src/decompositions/sparse-qr-solver.cpp | 2 +- src/decompositions/tridiagonalization.cpp | 2 +- src/solvers/solvers.cpp | 2 +- 31 files changed, 31 insertions(+), 31 deletions(-) diff --git a/include/eigenpy/decompositions/BDCSVD.hpp b/include/eigenpy/decompositions/BDCSVD.hpp index b73f352bd..9f9eb75ed 100644 --- a/include/eigenpy/decompositions/BDCSVD.hpp +++ b/include/eigenpy/decompositions/BDCSVD.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_bdcsvd_hpp__ diff --git a/include/eigenpy/decompositions/ComplexEigenSolver.hpp b/include/eigenpy/decompositions/ComplexEigenSolver.hpp index 8ab07a513..691455418 100644 --- a/include/eigenpy/decompositions/ComplexEigenSolver.hpp +++ b/include/eigenpy/decompositions/ComplexEigenSolver.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_complex_eigen_solver_hpp__ diff --git a/include/eigenpy/decompositions/ComplexSchur.hpp b/include/eigenpy/decompositions/ComplexSchur.hpp index 0199823a0..bac2ea1bd 100644 --- a/include/eigenpy/decompositions/ComplexSchur.hpp +++ b/include/eigenpy/decompositions/ComplexSchur.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_complex_schur_hpp__ diff --git a/include/eigenpy/decompositions/FullPivLU.hpp b/include/eigenpy/decompositions/FullPivLU.hpp index 0ef2cb3e2..910f327c8 100644 --- a/include/eigenpy/decompositions/FullPivLU.hpp +++ b/include/eigenpy/decompositions/FullPivLU.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020-2025 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_fullpivlu_hpp__ diff --git a/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp b/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp index d2b54820b..255470985 100644 --- a/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp +++ b/include/eigenpy/decompositions/GeneralizedEigenSolver.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_generalized_eigen_solver_hpp__ diff --git a/include/eigenpy/decompositions/GeneralizedSelfAdjointEigenSolver.hpp b/include/eigenpy/decompositions/GeneralizedSelfAdjointEigenSolver.hpp index 274d14c1b..7d23c7ac6 100644 --- a/include/eigenpy/decompositions/GeneralizedSelfAdjointEigenSolver.hpp +++ b/include/eigenpy/decompositions/GeneralizedSelfAdjointEigenSolver.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_generalized_self_adjoint_eigen_solver_hpp__ diff --git a/include/eigenpy/decompositions/HessenbergDecomposition.hpp b/include/eigenpy/decompositions/HessenbergDecomposition.hpp index e9a58aefb..a547a3fb7 100644 --- a/include/eigenpy/decompositions/HessenbergDecomposition.hpp +++ b/include/eigenpy/decompositions/HessenbergDecomposition.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_hessenberg_decomposition_hpp__ diff --git a/include/eigenpy/decompositions/JacobiSVD.hpp b/include/eigenpy/decompositions/JacobiSVD.hpp index 130b35a50..1ea45ed1d 100644 --- a/include/eigenpy/decompositions/JacobiSVD.hpp +++ b/include/eigenpy/decompositions/JacobiSVD.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_jacobisvd_hpp__ diff --git a/include/eigenpy/decompositions/PartialPivLU.hpp b/include/eigenpy/decompositions/PartialPivLU.hpp index 28bb3ba1b..f9b3ccc47 100644 --- a/include/eigenpy/decompositions/PartialPivLU.hpp +++ b/include/eigenpy/decompositions/PartialPivLU.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020-2025 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_partialpivlu_hpp__ diff --git a/include/eigenpy/decompositions/RealQZ.hpp b/include/eigenpy/decompositions/RealQZ.hpp index 1b339345f..76b45dbc2 100644 --- a/include/eigenpy/decompositions/RealQZ.hpp +++ b/include/eigenpy/decompositions/RealQZ.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_generalized_real_qz_hpp__ diff --git a/include/eigenpy/decompositions/RealSchur.hpp b/include/eigenpy/decompositions/RealSchur.hpp index 7c0be358d..ba0de9e59 100644 --- a/include/eigenpy/decompositions/RealSchur.hpp +++ b/include/eigenpy/decompositions/RealSchur.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_generalized_real_schur_hpp__ diff --git a/include/eigenpy/decompositions/SVDBase.hpp b/include/eigenpy/decompositions/SVDBase.hpp index 11c3e8d22..5c9d0e391 100644 --- a/include/eigenpy/decompositions/SVDBase.hpp +++ b/include/eigenpy/decompositions/SVDBase.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_svdbase_hpp__ diff --git a/include/eigenpy/decompositions/Tridiagonalization.hpp b/include/eigenpy/decompositions/Tridiagonalization.hpp index 5b8b78f17..d24deb988 100644 --- a/include/eigenpy/decompositions/Tridiagonalization.hpp +++ b/include/eigenpy/decompositions/Tridiagonalization.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2020 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_tridiagonalization_hpp__ diff --git a/include/eigenpy/decompositions/sparse/LU.hpp b/include/eigenpy/decompositions/sparse/LU.hpp index 5c1a6a979..d66b267fe 100644 --- a/include/eigenpy/decompositions/sparse/LU.hpp +++ b/include/eigenpy/decompositions/sparse/LU.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_sparse_lu_hpp__ diff --git a/include/eigenpy/decompositions/sparse/QR.hpp b/include/eigenpy/decompositions/sparse/QR.hpp index d0b26b12d..79145a7f3 100644 --- a/include/eigenpy/decompositions/sparse/QR.hpp +++ b/include/eigenpy/decompositions/sparse/QR.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_sparse_qr_hpp__ diff --git a/include/eigenpy/solvers/BiCGSTAB.hpp b/include/eigenpy/solvers/BiCGSTAB.hpp index af6c56787..061c88c5a 100644 --- a/include/eigenpy/solvers/BiCGSTAB.hpp +++ b/include/eigenpy/solvers/BiCGSTAB.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018 CNRS + * Copyright 2025 INRIA */ #ifndef __eigenpy_bicgstab_hpp__ diff --git a/include/eigenpy/solvers/IncompleteCholesky.hpp b/include/eigenpy/solvers/IncompleteCholesky.hpp index 302c6f78c..7e0e80d21 100644 --- a/include/eigenpy/solvers/IncompleteCholesky.hpp +++ b/include/eigenpy/solvers/IncompleteCholesky.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_sparse_incomplete_cholesky_hpp__ diff --git a/include/eigenpy/solvers/IncompleteLUT.hpp b/include/eigenpy/solvers/IncompleteLUT.hpp index 43edb18c0..27a77ef83 100644 --- a/include/eigenpy/solvers/IncompleteLUT.hpp +++ b/include/eigenpy/solvers/IncompleteLUT.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #ifndef __eigenpy_decompositions_sparse_incomplete_lut_hpp__ diff --git a/src/decompositions/bdcsvd-solver.cpp b/src/decompositions/bdcsvd-solver.cpp index c691a426f..2fda1b3fb 100644 --- a/src/decompositions/bdcsvd-solver.cpp +++ b/src/decompositions/bdcsvd-solver.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/BDCSVD.hpp" diff --git a/src/decompositions/complex-eigen-solver.cpp b/src/decompositions/complex-eigen-solver.cpp index 4bef19df5..99df394ad 100644 --- a/src/decompositions/complex-eigen-solver.cpp +++ b/src/decompositions/complex-eigen-solver.cpp @@ -1,6 +1,6 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/ComplexEigenSolver.hpp" diff --git a/src/decompositions/complex-schur.cpp b/src/decompositions/complex-schur.cpp index ece7c1954..555e345c2 100644 --- a/src/decompositions/complex-schur.cpp +++ b/src/decompositions/complex-schur.cpp @@ -1,6 +1,6 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/ComplexSchur.hpp" diff --git a/src/decompositions/generalized-eigen-solver.cpp b/src/decompositions/generalized-eigen-solver.cpp index 6b8a422ac..424393a70 100644 --- a/src/decompositions/generalized-eigen-solver.cpp +++ b/src/decompositions/generalized-eigen-solver.cpp @@ -1,6 +1,6 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/GeneralizedEigenSolver.hpp" diff --git a/src/decompositions/generalized-self-adjoint-eigen-solver.cpp b/src/decompositions/generalized-self-adjoint-eigen-solver.cpp index ac5330223..9dd254604 100644 --- a/src/decompositions/generalized-self-adjoint-eigen-solver.cpp +++ b/src/decompositions/generalized-self-adjoint-eigen-solver.cpp @@ -1,6 +1,6 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/GeneralizedSelfAdjointEigenSolver.hpp" diff --git a/src/decompositions/hessenberg-decomposition.cpp b/src/decompositions/hessenberg-decomposition.cpp index 9301f324c..6daebfc83 100644 --- a/src/decompositions/hessenberg-decomposition.cpp +++ b/src/decompositions/hessenberg-decomposition.cpp @@ -1,6 +1,6 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/HessenbergDecomposition.hpp" diff --git a/src/decompositions/jacobisvd-solver.cpp b/src/decompositions/jacobisvd-solver.cpp index 7abf93a63..a59538ca9 100644 --- a/src/decompositions/jacobisvd-solver.cpp +++ b/src/decompositions/jacobisvd-solver.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/JacobiSVD.hpp" diff --git a/src/decompositions/real-qz.cpp b/src/decompositions/real-qz.cpp index 2920024e3..0ed7561e8 100644 --- a/src/decompositions/real-qz.cpp +++ b/src/decompositions/real-qz.cpp @@ -1,6 +1,6 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/RealQZ.hpp" diff --git a/src/decompositions/real-schur.cpp b/src/decompositions/real-schur.cpp index 05f8b2402..dbfe0bd7b 100644 --- a/src/decompositions/real-schur.cpp +++ b/src/decompositions/real-schur.cpp @@ -1,6 +1,6 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/RealSchur.hpp" diff --git a/src/decompositions/sparse-lu-solver.cpp b/src/decompositions/sparse-lu-solver.cpp index 2c27a1e88..e92a4f5af 100644 --- a/src/decompositions/sparse-lu-solver.cpp +++ b/src/decompositions/sparse-lu-solver.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/sparse/LU.hpp" diff --git a/src/decompositions/sparse-qr-solver.cpp b/src/decompositions/sparse-qr-solver.cpp index 1460251bd..2657ec89e 100644 --- a/src/decompositions/sparse-qr-solver.cpp +++ b/src/decompositions/sparse-qr-solver.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/sparse/QR.hpp" diff --git a/src/decompositions/tridiagonalization.cpp b/src/decompositions/tridiagonalization.cpp index 25e1e3185..52febadd2 100644 --- a/src/decompositions/tridiagonalization.cpp +++ b/src/decompositions/tridiagonalization.cpp @@ -1,6 +1,6 @@ /* - * Copyright 2024 INRIA + * Copyright 2025 INRIA */ #include "eigenpy/decompositions/Tridiagonalization.hpp" diff --git a/src/solvers/solvers.cpp b/src/solvers/solvers.cpp index 58d39f350..44153b3b0 100644 --- a/src/solvers/solvers.cpp +++ b/src/solvers/solvers.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 CNRS INRIA + * Copyright 2017-2025 CNRS INRIA */ #include From d98d12c5e3039321db5811a5e6df49442decdcd1 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Wed, 23 Jul 2025 16:39:53 +0200 Subject: [PATCH 28/43] Add methods matrixU and matrixL in SParseLU --- include/eigenpy/decompositions/sparse/LU.hpp | 93 +++++++++++++++++-- src/decompositions/sparse-lu-solver.cpp | 14 +++ src/decompositions/sparse-qr-solver.cpp | 3 +- .../decompositions/sparse/test_SparseLU.py | 26 ++++++ 4 files changed, 129 insertions(+), 7 deletions(-) diff --git a/include/eigenpy/decompositions/sparse/LU.hpp b/include/eigenpy/decompositions/sparse/LU.hpp index d66b267fe..045b3f44e 100644 --- a/include/eigenpy/decompositions/sparse/LU.hpp +++ b/include/eigenpy/decompositions/sparse/LU.hpp @@ -14,6 +14,76 @@ namespace eigenpy { +template +struct SparseLUMatrixLReturnTypeVisitor + : public boost::python::def_visitor< + SparseLUMatrixLReturnTypeVisitor> { + typedef Eigen::SparseLUMatrixLReturnType LType; + typedef typename MappedSupernodalType::Scalar Scalar; + typedef Eigen::Matrix VectorXs; + typedef Eigen::Matrix + MatrixXs; + + template + static void solveInPlace(const LType &self, + Eigen::Ref mat_vec) { + self.solveInPlace(mat_vec); + } + + template + void visit(PyClass &cl) const { + cl.def(bp::init(bp::args("self", "mapL"))) + + .def("rows", <ype::rows) + .def("cols", <ype::cols) + + .def("solveInPlace", &solveInPlace, bp::args("self", "X")) + .def("solveInPlace", &solveInPlace, bp::args("self", "x")); + } + + static void expose(const std::string &name) { + bp::class_(name.c_str(), "Eigen SparseLUMatrixLReturnType", + bp::no_init) + .def(SparseLUMatrixLReturnTypeVisitor()) + .def(IdVisitor()); + } +}; + +template +struct SparseLUMatrixUReturnTypeVisitor + : public boost::python::def_visitor< + SparseLUMatrixUReturnTypeVisitor> { + typedef Eigen::SparseLUMatrixUReturnType UType; + typedef typename MatrixLType::Scalar Scalar; + typedef Eigen::Matrix VectorXs; + typedef Eigen::Matrix + MatrixXs; + + template + static void solveInPlace(const UType &self, + Eigen::Ref mat_vec) { + self.solveInPlace(mat_vec); + } + + template + void visit(PyClass &cl) const { + cl.def(bp::init(bp::args("self", "mapL", "mapU"))) + + .def("rows", &UType::rows) + .def("cols", &UType::cols) + + .def("solveInPlace", &solveInPlace, bp::args("self", "X")) + .def("solveInPlace", &solveInPlace, bp::args("self", "x")); + } + + static void expose(const std::string &name) { + bp::class_(name.c_str(), "Eigen SparseLUMatrixUReturnType", + bp::no_init) + .def(SparseLUMatrixUReturnTypeVisitor()) + .def(IdVisitor()); + } +}; + template > @@ -25,11 +95,13 @@ struct SparseLUVisitor : public boost::python::def_visitor< typedef Eigen::SparseLU Solver; typedef typename MatrixType::Scalar Scalar; typedef typename MatrixType::RealScalar RealScalar; - typedef Eigen::Matrix - DenseVectorXs; - typedef Eigen::Matrix - DenseMatrixXs; + + typedef typename Solver::SCMatrix SCMatrix; + typedef typename MatrixType::StorageIndex StorageIndex; + typedef Eigen::MappedSparseMatrix + MappedSparseMatrix; + typedef Eigen::SparseLUMatrixLReturnType LType; + typedef Eigen::SparseLUMatrixUReturnType UType; template void visit(PyClass &cl) const { @@ -57,7 +129,16 @@ struct SparseLUVisitor : public boost::python::def_visitor< .def("nnzL", &Solver::nnzL, bp::arg("self"), "The number of non zero elements in L") .def("nnzU", &Solver::nnzU, bp::arg("self"), - "The number of non zero elements in L") + "The number of non zero elements in U") + + .def( + "matrixL", + +[](const Solver &self) -> LType { return self.matrixL(); }, + "Returns an expression of the matrix L.") + .def( + "matrixU", + +[](const Solver &self) -> UType { return self.matrixU(); }, + "Returns an expression of the matrix U.") .def("colsPermutation", &Solver::colsPermutation, bp::arg("self"), "Returns a reference to the column matrix permutation PTc such " diff --git a/src/decompositions/sparse-lu-solver.cpp b/src/decompositions/sparse-lu-solver.cpp index e92a4f5af..b5fe0165b 100644 --- a/src/decompositions/sparse-lu-solver.cpp +++ b/src/decompositions/sparse-lu-solver.cpp @@ -7,7 +7,21 @@ namespace eigenpy { void exposeSparseLUSolver() { using namespace Eigen; + typedef SparseMatrix ColMajorSparseMatrix; + typedef typename ColMajorSparseMatrix::StorageIndex StorageIndex; + typedef COLAMDOrdering Ordering; + typedef SparseLU SparseLUType; + + typedef typename SparseLUType::Scalar Scalar; + typedef typename SparseLUType::SCMatrix SCMatrix; + typedef Eigen::MappedSparseMatrix + MappedSparseMatrix; + + SparseLUMatrixLReturnTypeVisitor::expose( + ("SparseLUMatrixLReturnType")); + SparseLUMatrixUReturnTypeVisitor::expose( + ("SparseLUMatrixUReturnType")); SparseLUVisitor::expose("SparseLU"); } } // namespace eigenpy diff --git a/src/decompositions/sparse-qr-solver.cpp b/src/decompositions/sparse-qr-solver.cpp index 2657ec89e..61745e742 100644 --- a/src/decompositions/sparse-qr-solver.cpp +++ b/src/decompositions/sparse-qr-solver.cpp @@ -9,7 +9,8 @@ void exposeSparseQRSolver() { using namespace Eigen; typedef SparseMatrix ColMajorSparseMatrix; - typedef COLAMDOrdering Ordering; + typedef typename ColMajorSparseMatrix::StorageIndex StorageIndex; + typedef COLAMDOrdering Ordering; typedef SparseQR SparseQRType; SparseQRMatrixQTransposeReturnTypeVisitor::expose( diff --git a/unittest/python/decompositions/sparse/test_SparseLU.py b/unittest/python/decompositions/sparse/test_SparseLU.py index 55f0b3ae2..ee0cd7f3e 100644 --- a/unittest/python/decompositions/sparse/test_SparseLU.py +++ b/unittest/python/decompositions/sparse/test_SparseLU.py @@ -36,3 +36,29 @@ assert isinstance(X_est, spa.csc_matrix) assert eigenpy.is_approx(X_est.toarray(), X_sparse.toarray()) assert eigenpy.is_approx(A.dot(X_est.toarray()), B_sparse.toarray()) + +assert splu.nnzL() > 0 +assert splu.nnzU() > 0 + +L = splu.matrixL() +U = splu.matrixU() + +assert L.rows() == dim +assert L.cols() == dim +assert U.rows() == dim +assert U.cols() == dim + +x_true = rng.random(dim) +b_true = A.dot(x_true) +P_rows_indices = splu.rowsPermutation().indices() +P_cols_indices = splu.colsPermutation().indices() + +b_permuted = b_true[P_rows_indices] +z = b_permuted.copy() +L.solveInPlace(z) +y = z.copy() +U.solveInPlace(y) +x_reconstructed = np.zeros(dim) +x_reconstructed[P_cols_indices] = y + +assert eigenpy.is_approx(x_reconstructed, x_true, prec=1e-6) From 30dfff92cb178256fe9ef912b77add2ff8968f90 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Wed, 23 Jul 2025 16:49:43 +0200 Subject: [PATCH 29/43] Capital letters in header names in sparse decompositions --- CMakeLists.txt | 9 +++++---- .../sparse/{LDLT.hpp => SimplicialLDLT.hpp} | 0 .../decompositions/sparse/{LLT.hpp => SimplicialLLT.hpp} | 0 .../decompositions/sparse/{LU.hpp => SparseLU.hpp} | 0 .../decompositions/sparse/{QR.hpp => SparseQR.hpp} | 0 .../sparse/accelerate/{accelerate.hpp => Accelerate.hpp} | 0 src/decompositions/accelerate.cpp | 2 +- src/decompositions/simplicial-ldlt-solver.cpp | 2 +- src/decompositions/simplicial-llt-solver.cpp | 2 +- src/decompositions/sparse-lu-solver.cpp | 2 +- src/decompositions/sparse-qr-solver.cpp | 2 +- 11 files changed, 10 insertions(+), 9 deletions(-) rename include/eigenpy/decompositions/sparse/{LDLT.hpp => SimplicialLDLT.hpp} (100%) rename include/eigenpy/decompositions/sparse/{LLT.hpp => SimplicialLLT.hpp} (100%) rename include/eigenpy/decompositions/sparse/{LU.hpp => SparseLU.hpp} (100%) rename include/eigenpy/decompositions/sparse/{QR.hpp => SparseQR.hpp} (100%) rename include/eigenpy/decompositions/sparse/accelerate/{accelerate.hpp => Accelerate.hpp} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ccc9f4b3..a1c95ac9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -209,12 +209,13 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_CHOLMOD_HEADERS include/eigenpy/decompositions/sparse/cholmod/CholmodSupernodalLLT.hpp) set(${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_ACCELERATE_HEADERS - include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp) + include/eigenpy/decompositions/sparse/accelerate/Accelerate.hpp) set(${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_HEADERS - include/eigenpy/decompositions/sparse/LLT.hpp - include/eigenpy/decompositions/sparse/LDLT.hpp - include/eigenpy/decompositions/sparse/LU.hpp + include/eigenpy/decompositions/sparse/SimplicialLLT.hpp + include/eigenpy/decompositions/sparse/SimplicialLDLT.hpp + include/eigenpy/decompositions/sparse/SparseLU.hpp + include/eigenpy/decompositions/sparse/SparseQR.hpp include/eigenpy/decompositions/sparse/SimplicialCholesky.hpp include/eigenpy/decompositions/sparse/SparseSolverBase.hpp) diff --git a/include/eigenpy/decompositions/sparse/LDLT.hpp b/include/eigenpy/decompositions/sparse/SimplicialLDLT.hpp similarity index 100% rename from include/eigenpy/decompositions/sparse/LDLT.hpp rename to include/eigenpy/decompositions/sparse/SimplicialLDLT.hpp diff --git a/include/eigenpy/decompositions/sparse/LLT.hpp b/include/eigenpy/decompositions/sparse/SimplicialLLT.hpp similarity index 100% rename from include/eigenpy/decompositions/sparse/LLT.hpp rename to include/eigenpy/decompositions/sparse/SimplicialLLT.hpp diff --git a/include/eigenpy/decompositions/sparse/LU.hpp b/include/eigenpy/decompositions/sparse/SparseLU.hpp similarity index 100% rename from include/eigenpy/decompositions/sparse/LU.hpp rename to include/eigenpy/decompositions/sparse/SparseLU.hpp diff --git a/include/eigenpy/decompositions/sparse/QR.hpp b/include/eigenpy/decompositions/sparse/SparseQR.hpp similarity index 100% rename from include/eigenpy/decompositions/sparse/QR.hpp rename to include/eigenpy/decompositions/sparse/SparseQR.hpp diff --git a/include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp b/include/eigenpy/decompositions/sparse/accelerate/Accelerate.hpp similarity index 100% rename from include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp rename to include/eigenpy/decompositions/sparse/accelerate/Accelerate.hpp diff --git a/src/decompositions/accelerate.cpp b/src/decompositions/accelerate.cpp index 8f2f05c16..ae688a2e0 100644 --- a/src/decompositions/accelerate.cpp +++ b/src/decompositions/accelerate.cpp @@ -5,7 +5,7 @@ #include "eigenpy/fwd.hpp" #include "eigenpy/decompositions/decompositions.hpp" -#include "eigenpy/decompositions/sparse/accelerate/accelerate.hpp" +#include "eigenpy/decompositions/sparse/accelerate/Accelerate.hpp" namespace eigenpy { diff --git a/src/decompositions/simplicial-ldlt-solver.cpp b/src/decompositions/simplicial-ldlt-solver.cpp index c9a3e7b98..1d090afeb 100644 --- a/src/decompositions/simplicial-ldlt-solver.cpp +++ b/src/decompositions/simplicial-ldlt-solver.cpp @@ -2,7 +2,7 @@ * Copyright 2024 INRIA */ -#include "eigenpy/decompositions/sparse/LDLT.hpp" +#include "eigenpy/decompositions/sparse/SimplicialLDLT.hpp" namespace eigenpy { void exposeSimplicialLDLTSolver() { diff --git a/src/decompositions/simplicial-llt-solver.cpp b/src/decompositions/simplicial-llt-solver.cpp index f46e36f25..70f069970 100644 --- a/src/decompositions/simplicial-llt-solver.cpp +++ b/src/decompositions/simplicial-llt-solver.cpp @@ -2,7 +2,7 @@ * Copyright 2024 INRIA */ -#include "eigenpy/decompositions/sparse/LLT.hpp" +#include "eigenpy/decompositions/sparse/SimplicialLLT.hpp" namespace eigenpy { void exposeSimplicialLLTSolver() { diff --git a/src/decompositions/sparse-lu-solver.cpp b/src/decompositions/sparse-lu-solver.cpp index b5fe0165b..763a75592 100644 --- a/src/decompositions/sparse-lu-solver.cpp +++ b/src/decompositions/sparse-lu-solver.cpp @@ -2,7 +2,7 @@ * Copyright 2025 INRIA */ -#include "eigenpy/decompositions/sparse/LU.hpp" +#include "eigenpy/decompositions/sparse/SparseLU.hpp" namespace eigenpy { void exposeSparseLUSolver() { diff --git a/src/decompositions/sparse-qr-solver.cpp b/src/decompositions/sparse-qr-solver.cpp index 61745e742..0bac14197 100644 --- a/src/decompositions/sparse-qr-solver.cpp +++ b/src/decompositions/sparse-qr-solver.cpp @@ -2,7 +2,7 @@ * Copyright 2025 INRIA */ -#include "eigenpy/decompositions/sparse/QR.hpp" +#include "eigenpy/decompositions/sparse/SparseQR.hpp" namespace eigenpy { void exposeSparseQRSolver() { From a508351ef5e94e792b029158104d9f6a4ebfbfe6 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 24 Jul 2025 16:38:49 +0200 Subject: [PATCH 30/43] CMakeLists: Added FullPivLU and PartialPivLU headers --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index a1c95ac9e..f51198127 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -241,6 +241,8 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_HEADERS include/eigenpy/decompositions/RealSchur.hpp include/eigenpy/decompositions/ComplexEigenSolver.hpp include/eigenpy/decompositions/ComplexSchur.hpp + include/eigenpy/decompositions/FullPivLU.hpp + include/eigenpy/decompositions/PartialPivLU.hpp include/eigenpy/decompositions/PermutationMatrix.hpp include/eigenpy/decompositions/LDLT.hpp include/eigenpy/decompositions/LLT.hpp From b8774a1c005f8afeda5777725a830321c30c1d02 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 24 Jul 2025 16:44:56 +0200 Subject: [PATCH 31/43] Removed outpassed guards regarding eigen version in decompositions and solvers --- include/eigenpy/decompositions/FullPivLU.hpp | 2 -- include/eigenpy/decompositions/PartialPivLU.hpp | 2 -- src/solvers/solvers.cpp | 2 -- 3 files changed, 6 deletions(-) diff --git a/include/eigenpy/decompositions/FullPivLU.hpp b/include/eigenpy/decompositions/FullPivLU.hpp index 910f327c8..1a6c3788c 100644 --- a/include/eigenpy/decompositions/FullPivLU.hpp +++ b/include/eigenpy/decompositions/FullPivLU.hpp @@ -96,11 +96,9 @@ struct FullPivLUSolverVisitor .def("rank", &Solver::rank, bp::arg("self")) -#if EIGEN_VERSION_AT_LEAST(3, 3, 0) .def("rcond", &Solver::rcond, bp::arg("self"), "Returns an estimate of the reciprocal condition number of the " "matrix.") -#endif .def("reconstructedMatrix", &Solver::reconstructedMatrix, bp::arg("self"), "Returns the matrix represented by the decomposition, i.e., it " diff --git a/include/eigenpy/decompositions/PartialPivLU.hpp b/include/eigenpy/decompositions/PartialPivLU.hpp index f9b3ccc47..2c986a831 100644 --- a/include/eigenpy/decompositions/PartialPivLU.hpp +++ b/include/eigenpy/decompositions/PartialPivLU.hpp @@ -63,11 +63,9 @@ struct PartialPivLUSolverVisitor : public boost::python::def_visitor< "Returns the permutation P.", bp::return_value_policy()) -#if EIGEN_VERSION_AT_LEAST(3, 3, 0) .def("rcond", &Solver::rcond, bp::arg("self"), "Returns an estimate of the reciprocal condition number of the " "matrix.") -#endif .def("reconstructedMatrix", &Solver::reconstructedMatrix, bp::arg("self"), "Returns the matrix represented by the decomposition, i.e., it " diff --git a/src/solvers/solvers.cpp b/src/solvers/solvers.cpp index 44153b3b0..f1faee195 100644 --- a/src/solvers/solvers.cpp +++ b/src/solvers/solvers.cpp @@ -46,7 +46,6 @@ void exposeSolvers() { ConjugateGradientVisitor::expose( "IdentityConjugateGradient"); -#if EIGEN_VERSION_AT_LEAST(3, 3, 5) LeastSquaresConjugateGradientVisitor>>:: expose("LeastSquaresConjugateGradient"); @@ -54,7 +53,6 @@ void exposeSolvers() { expose("IdentityLeastSquaresConjugateGradient"); LeastSquaresConjugateGradientVisitor:: expose("DiagonalLeastSquaresConjugateGradient"); -#endif BiCGSTABVisitor>::expose("BiCGSTAB"); BiCGSTABVisitor::expose("IdentityBiCGSTAB"); From ff0fc2565f5cf711135dea0f0632257b0f36b2b6 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 24 Jul 2025 16:50:33 +0200 Subject: [PATCH 32/43] Include guards: Set name that corresopnds to directory --- include/eigenpy/solvers/BFGSPreconditioners.hpp | 6 +++--- include/eigenpy/solvers/BasicPreconditioners.hpp | 6 +++--- include/eigenpy/solvers/BiCGSTAB.hpp | 6 +++--- include/eigenpy/solvers/ConjugateGradient.hpp | 6 +++--- include/eigenpy/solvers/IncompleteCholesky.hpp | 6 +++--- include/eigenpy/solvers/IncompleteLUT.hpp | 6 +++--- include/eigenpy/solvers/IterativeSolverBase.hpp | 6 +++--- include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp | 6 +++--- include/eigenpy/solvers/MINRES.hpp | 6 +++--- include/eigenpy/solvers/SparseSolverBase.hpp | 6 +++--- include/eigenpy/solvers/preconditioners.hpp | 6 +++--- include/eigenpy/solvers/solvers.hpp | 6 +++--- 12 files changed, 36 insertions(+), 36 deletions(-) diff --git a/include/eigenpy/solvers/BFGSPreconditioners.hpp b/include/eigenpy/solvers/BFGSPreconditioners.hpp index 40da2b584..7a73a9db3 100644 --- a/include/eigenpy/solvers/BFGSPreconditioners.hpp +++ b/include/eigenpy/solvers/BFGSPreconditioners.hpp @@ -3,8 +3,8 @@ * Copyright 2024 Inria */ -#ifndef __eigenpy_bfgs_preconditioners_hpp__ -#define __eigenpy_bfgs_preconditioners_hpp__ +#ifndef __eigenpy_solvers_bfgs_preconditioners_hpp__ +#define __eigenpy_solvers_bfgs_preconditioners_hpp__ #include @@ -65,4 +65,4 @@ struct LimitedBFGSPreconditionerBaseVisitor } // namespace eigenpy -#endif // ifndef __eigenpy_bfgs_preconditioners_hpp__ +#endif // ifndef __eigenpy_solvers_bfgs_preconditioners_hpp__ diff --git a/include/eigenpy/solvers/BasicPreconditioners.hpp b/include/eigenpy/solvers/BasicPreconditioners.hpp index 7e4cd7190..473e73d29 100644 --- a/include/eigenpy/solvers/BasicPreconditioners.hpp +++ b/include/eigenpy/solvers/BasicPreconditioners.hpp @@ -3,8 +3,8 @@ * Copyright 2024 Inria */ -#ifndef __eigenpy_basic_preconditioners_hpp__ -#define __eigenpy_basic_preconditioners_hpp__ +#ifndef __eigenpy_solvers_basic_preconditioners_hpp__ +#define __eigenpy_solvers_basic_preconditioners_hpp__ #include @@ -115,4 +115,4 @@ struct IdentityPreconditionerVisitor } // namespace eigenpy -#endif // ifndef __eigenpy_basic_preconditioners_hpp__ +#endif // ifndef __eigenpy_solvers_basic_preconditioners_hpp__ diff --git a/include/eigenpy/solvers/BiCGSTAB.hpp b/include/eigenpy/solvers/BiCGSTAB.hpp index 061c88c5a..41bd4bf3c 100644 --- a/include/eigenpy/solvers/BiCGSTAB.hpp +++ b/include/eigenpy/solvers/BiCGSTAB.hpp @@ -2,8 +2,8 @@ * Copyright 2025 INRIA */ -#ifndef __eigenpy_bicgstab_hpp__ -#define __eigenpy_bicgstab_hpp__ +#ifndef __eigenpy_solvers_bicgstab_hpp__ +#define __eigenpy_solvers_bicgstab_hpp__ #include @@ -38,4 +38,4 @@ struct BiCGSTABVisitor } // namespace eigenpy -#endif // ifndef __eigenpy_bicgstab_hpp__ +#endif // ifndef __eigenpy_solvers_bicgstab_hpp__ diff --git a/include/eigenpy/solvers/ConjugateGradient.hpp b/include/eigenpy/solvers/ConjugateGradient.hpp index f2fdf8699..112ce3e84 100644 --- a/include/eigenpy/solvers/ConjugateGradient.hpp +++ b/include/eigenpy/solvers/ConjugateGradient.hpp @@ -2,8 +2,8 @@ * Copyright 2017 CNRS */ -#ifndef __eigenpy_conjugate_gradient_hpp__ -#define __eigenpy_conjugate_gradient_hpp__ +#ifndef __eigenpy_solvers_conjugate_gradient_hpp__ +#define __eigenpy_solvers_conjugate_gradient_hpp__ #include @@ -38,4 +38,4 @@ struct ConjugateGradientVisitor } // namespace eigenpy -#endif // ifndef __eigenpy_conjugate_gradient_hpp__ +#endif // ifndef __eigenpy_solvers_conjugate_gradient_hpp__ diff --git a/include/eigenpy/solvers/IncompleteCholesky.hpp b/include/eigenpy/solvers/IncompleteCholesky.hpp index 7e0e80d21..f0f1e30ca 100644 --- a/include/eigenpy/solvers/IncompleteCholesky.hpp +++ b/include/eigenpy/solvers/IncompleteCholesky.hpp @@ -2,8 +2,8 @@ * Copyright 2025 INRIA */ -#ifndef __eigenpy_decompositions_sparse_incomplete_cholesky_hpp__ -#define __eigenpy_decompositions_sparse_incomplete_cholesky_hpp__ +#ifndef __eigenpy_solvers_incomplete_cholesky_hpp__ +#define __eigenpy_solvers_incomplete_cholesky_hpp__ #include "eigenpy/eigenpy.hpp" #include "eigenpy/utils/scalar-name.hpp" @@ -122,4 +122,4 @@ struct IncompleteCholeskyVisitor : public boost::python::def_visitor< } // namespace eigenpy -#endif // ifndef __eigenpy_decompositions_sparse_incomplete_cholesky_hpp__ +#endif // ifndef __eigenpy_solvers_incomplete_cholesky_hpp__ diff --git a/include/eigenpy/solvers/IncompleteLUT.hpp b/include/eigenpy/solvers/IncompleteLUT.hpp index 27a77ef83..04c58ff74 100644 --- a/include/eigenpy/solvers/IncompleteLUT.hpp +++ b/include/eigenpy/solvers/IncompleteLUT.hpp @@ -2,8 +2,8 @@ * Copyright 2025 INRIA */ -#ifndef __eigenpy_decompositions_sparse_incomplete_lut_hpp__ -#define __eigenpy_decompositions_sparse_incomplete_lut_hpp__ +#ifndef __eigenpy_solvers_incomplete_lut_hpp__ +#define __eigenpy_solvers_incomplete_lut_hpp__ #include "eigenpy/eigenpy.hpp" #include "eigenpy/utils/scalar-name.hpp" @@ -105,4 +105,4 @@ struct IncompleteLUTVisitor } // namespace eigenpy -#endif // ifndef __eigenpy_decompositions_sparse_incomplete_lut_hpp__ +#endif // ifndef __eigenpy_solvers_incomplete_lut_hpp__ diff --git a/include/eigenpy/solvers/IterativeSolverBase.hpp b/include/eigenpy/solvers/IterativeSolverBase.hpp index 670bde3d2..426a9b89e 100644 --- a/include/eigenpy/solvers/IterativeSolverBase.hpp +++ b/include/eigenpy/solvers/IterativeSolverBase.hpp @@ -2,8 +2,8 @@ * Copyright 2017 CNRS */ -#ifndef __eigenpy_iterative_solver_base_hpp__ -#define __eigenpy_iterative_solver_base_hpp__ +#ifndef __eigenpy_solvers_iterative_solver_base_hpp__ +#define __eigenpy_solvers_iterative_solver_base_hpp__ #include "eigenpy/fwd.hpp" #include "eigenpy/solvers/SparseSolverBase.hpp" @@ -103,4 +103,4 @@ struct IterativeSolverVisitor : public boost::python::def_visitor< } // namespace eigenpy -#endif // ifndef __eigenpy_iterative_solver_base_hpp__ +#endif // ifndef __eigenpy_solvers_iterative_solver_base_hpp__ diff --git a/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp b/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp index 420431d2d..5f8212848 100644 --- a/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp +++ b/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp @@ -2,8 +2,8 @@ * Copyright 2017-2018 CNRS */ -#ifndef __eigenpy_least_square_conjugate_gradient_hpp__ -#define __eigenpy_least_square_conjugate_gradient_hpp__ +#ifndef __eigenpy_solvers_least_square_conjugate_gradient_hpp__ +#define __eigenpy_solvers_least_square_conjugate_gradient_hpp__ #include @@ -42,4 +42,4 @@ struct LeastSquaresConjugateGradientVisitor } // namespace eigenpy -#endif // ifndef __eigenpy_least_square_conjugate_gradient_hpp__ +#endif // ifndef __eigenpy_solvers_least_square_conjugate_gradient_hpp__ diff --git a/include/eigenpy/solvers/MINRES.hpp b/include/eigenpy/solvers/MINRES.hpp index fa53870e9..d7cf8133c 100644 --- a/include/eigenpy/solvers/MINRES.hpp +++ b/include/eigenpy/solvers/MINRES.hpp @@ -2,8 +2,8 @@ * Copyright 2021 INRIA */ -#ifndef __eigenpy_decompositions_minres_hpp__ -#define __eigenpy_decompositions_minres_hpp__ +#ifndef __eigenpy_solvers_minres_hpp__ +#define __eigenpy_solvers_minres_hpp__ #include #include @@ -178,4 +178,4 @@ struct MINRESSolverVisitor } // namespace eigenpy -#endif // ifndef __eigenpy_decompositions_minres_hpp__ +#endif // ifndef __eigenpy_solvers_minres_hpp__ diff --git a/include/eigenpy/solvers/SparseSolverBase.hpp b/include/eigenpy/solvers/SparseSolverBase.hpp index f4f2bdad1..97167b998 100644 --- a/include/eigenpy/solvers/SparseSolverBase.hpp +++ b/include/eigenpy/solvers/SparseSolverBase.hpp @@ -2,8 +2,8 @@ * Copyright 2017 CNRS */ -#ifndef __eigenpy_sparse_solver_base_hpp__ -#define __eigenpy_sparse_solver_base_hpp__ +#ifndef __eigenpy_solvers_sparse_solver_base_hpp__ +#define __eigenpy_solvers_sparse_solver_base_hpp__ #include "eigenpy/fwd.hpp" @@ -29,4 +29,4 @@ struct SparseSolverVisitor } // namespace eigenpy -#endif // ifndef __eigenpy_sparse_solver_base_hpp__ +#endif // ifndef __eigenpy_solvers_sparse_solver_base_hpp__ diff --git a/include/eigenpy/solvers/preconditioners.hpp b/include/eigenpy/solvers/preconditioners.hpp index 3e26638ba..0f5f25806 100644 --- a/include/eigenpy/solvers/preconditioners.hpp +++ b/include/eigenpy/solvers/preconditioners.hpp @@ -2,8 +2,8 @@ * Copyright 2017 CNRS */ -#ifndef __eigenpy_preconditioners_hpp__ -#define __eigenpy_preconditioners_hpp__ +#ifndef __eigenpy_solvers_preconditioners_hpp__ +#define __eigenpy_solvers_preconditioners_hpp__ #include "eigenpy/config.hpp" @@ -13,4 +13,4 @@ void EIGENPY_DLLAPI exposePreconditioners(); } // namespace eigenpy -#endif // define __eigenpy_preconditioners_hpp__ +#endif // define __eigenpy_solvers_preconditioners_hpp__ diff --git a/include/eigenpy/solvers/solvers.hpp b/include/eigenpy/solvers/solvers.hpp index af1a375c3..d7cc316b4 100644 --- a/include/eigenpy/solvers/solvers.hpp +++ b/include/eigenpy/solvers/solvers.hpp @@ -2,8 +2,8 @@ * Copyright 2017-2020 CNRS INRIA */ -#ifndef __eigenpy_solvers_hpp__ -#define __eigenpy_solvers_hpp__ +#ifndef __eigenpy_solvers_solvers_hpp__ +#define __eigenpy_solvers_solvers_hpp__ #include "eigenpy/config.hpp" @@ -14,4 +14,4 @@ void EIGENPY_DLLAPI exposeSolvers(); } // namespace eigenpy -#endif // define __eigenpy_solvers_hpp__ +#endif // define __eigenpy_solvers_solvers_hpp__ From 4908ccad175dc2f118e3f6ac089ba82453f84983 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 24 Jul 2025 16:54:26 +0200 Subject: [PATCH 33/43] Include guards: Updated date in copyright --- include/eigenpy/solvers/BFGSPreconditioners.hpp | 2 +- include/eigenpy/solvers/BasicPreconditioners.hpp | 2 +- include/eigenpy/solvers/ConjugateGradient.hpp | 1 + include/eigenpy/solvers/IterativeSolverBase.hpp | 1 + include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp | 1 + include/eigenpy/solvers/MINRES.hpp | 2 +- include/eigenpy/solvers/SparseSolverBase.hpp | 1 + include/eigenpy/solvers/preconditioners.hpp | 1 + include/eigenpy/solvers/solvers.hpp | 2 +- 9 files changed, 9 insertions(+), 4 deletions(-) diff --git a/include/eigenpy/solvers/BFGSPreconditioners.hpp b/include/eigenpy/solvers/BFGSPreconditioners.hpp index 7a73a9db3..2c591bb34 100644 --- a/include/eigenpy/solvers/BFGSPreconditioners.hpp +++ b/include/eigenpy/solvers/BFGSPreconditioners.hpp @@ -1,6 +1,6 @@ /* * Copyright 2017 CNRS - * Copyright 2024 Inria + * Copyright 2024-2025 INRIA */ #ifndef __eigenpy_solvers_bfgs_preconditioners_hpp__ diff --git a/include/eigenpy/solvers/BasicPreconditioners.hpp b/include/eigenpy/solvers/BasicPreconditioners.hpp index 473e73d29..2f4a0a242 100644 --- a/include/eigenpy/solvers/BasicPreconditioners.hpp +++ b/include/eigenpy/solvers/BasicPreconditioners.hpp @@ -1,6 +1,6 @@ /* * Copyright 2017 CNRS - * Copyright 2024 Inria + * Copyright 2024-2025 INRIA */ #ifndef __eigenpy_solvers_basic_preconditioners_hpp__ diff --git a/include/eigenpy/solvers/ConjugateGradient.hpp b/include/eigenpy/solvers/ConjugateGradient.hpp index 112ce3e84..2c99b277a 100644 --- a/include/eigenpy/solvers/ConjugateGradient.hpp +++ b/include/eigenpy/solvers/ConjugateGradient.hpp @@ -1,5 +1,6 @@ /* * Copyright 2017 CNRS + * Copyright 2025 INRIA */ #ifndef __eigenpy_solvers_conjugate_gradient_hpp__ diff --git a/include/eigenpy/solvers/IterativeSolverBase.hpp b/include/eigenpy/solvers/IterativeSolverBase.hpp index 426a9b89e..ebab5763a 100644 --- a/include/eigenpy/solvers/IterativeSolverBase.hpp +++ b/include/eigenpy/solvers/IterativeSolverBase.hpp @@ -1,5 +1,6 @@ /* * Copyright 2017 CNRS + * Copyright 2025 INRIA */ #ifndef __eigenpy_solvers_iterative_solver_base_hpp__ diff --git a/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp b/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp index 5f8212848..2f3aae644 100644 --- a/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp +++ b/include/eigenpy/solvers/LeastSquaresConjugateGradient.hpp @@ -1,5 +1,6 @@ /* * Copyright 2017-2018 CNRS + * Copyright 2025 INRIA */ #ifndef __eigenpy_solvers_least_square_conjugate_gradient_hpp__ diff --git a/include/eigenpy/solvers/MINRES.hpp b/include/eigenpy/solvers/MINRES.hpp index d7cf8133c..c21fcb66d 100644 --- a/include/eigenpy/solvers/MINRES.hpp +++ b/include/eigenpy/solvers/MINRES.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2021 INRIA + * Copyright 2021-2025 INRIA */ #ifndef __eigenpy_solvers_minres_hpp__ diff --git a/include/eigenpy/solvers/SparseSolverBase.hpp b/include/eigenpy/solvers/SparseSolverBase.hpp index 97167b998..795d98eef 100644 --- a/include/eigenpy/solvers/SparseSolverBase.hpp +++ b/include/eigenpy/solvers/SparseSolverBase.hpp @@ -1,5 +1,6 @@ /* * Copyright 2017 CNRS + * Copyright 2025 INRIA */ #ifndef __eigenpy_solvers_sparse_solver_base_hpp__ diff --git a/include/eigenpy/solvers/preconditioners.hpp b/include/eigenpy/solvers/preconditioners.hpp index 0f5f25806..c537e2779 100644 --- a/include/eigenpy/solvers/preconditioners.hpp +++ b/include/eigenpy/solvers/preconditioners.hpp @@ -1,5 +1,6 @@ /* * Copyright 2017 CNRS + * Copyright 2025 INRIA */ #ifndef __eigenpy_solvers_preconditioners_hpp__ diff --git a/include/eigenpy/solvers/solvers.hpp b/include/eigenpy/solvers/solvers.hpp index d7cc316b4..16e1fddce 100644 --- a/include/eigenpy/solvers/solvers.hpp +++ b/include/eigenpy/solvers/solvers.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 CNRS INRIA + * Copyright 2017-2025 CNRS INRIA */ #ifndef __eigenpy_solvers_solvers_hpp__ From 59619e1cb1b62b31efe5b2d158a9ed566d51e730 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 24 Jul 2025 17:11:05 +0200 Subject: [PATCH 34/43] MINRES: Backward compatibility to call from eigenpy and eigenpy.solvers --- src/decompositions/decompositions.cpp | 8 ++++++++ unittest/python/test_MINRES.py | 9 +++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/decompositions/decompositions.cpp b/src/decompositions/decompositions.cpp index d982a5763..85fb81061 100644 --- a/src/decompositions/decompositions.cpp +++ b/src/decompositions/decompositions.cpp @@ -3,6 +3,7 @@ */ #include "eigenpy/decompositions/decompositions.hpp" +#include "eigenpy/solvers/MINRES.hpp" #include "eigenpy/fwd.hpp" @@ -31,6 +32,11 @@ void exposePermutationMatrix(); void exposeBDCSVDSolver(); void exposeJacobiSVDSolver(); +void exposeBackwardCompatibilityAliases() { + typedef Eigen::MatrixXd MatrixXd; + MINRESSolverVisitor::expose("MINRES"); +} + void exposeDecompositions() { using namespace Eigen; @@ -80,5 +86,7 @@ void exposeDecompositions() { #ifdef EIGENPY_WITH_ACCELERATE_SUPPORT exposeAccelerate(); #endif + + exposeBackwardCompatibilityAliases(); } } // namespace eigenpy diff --git a/unittest/python/test_MINRES.py b/unittest/python/test_MINRES.py index 9e22fbcf7..c830a4f77 100644 --- a/unittest/python/test_MINRES.py +++ b/unittest/python/test_MINRES.py @@ -11,6 +11,11 @@ X = rng.random((dim, 20)) B = A.dot(X) X_est = minres.solve(B) -print("A.dot(X_est):", A.dot(X_est)) -print("B:", B) +assert eigenpy.is_approx(A.dot(X_est), B, 1e-6) + +minres_back = eigenpy.MINRES(A) + +X = rng.random((dim, 20)) +B = A.dot(X) +X_est = minres_back.solve(B) assert eigenpy.is_approx(A.dot(X_est), B, 1e-6) From 060253bf33bb5ca43232c15d7aa4a9003f21b5c6 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 24 Jul 2025 18:53:03 +0200 Subject: [PATCH 35/43] Split solvers.cpp into separated .cpp as in decompositions --- CMakeLists.txt | 11 ++- src/solvers/bicgstab.cpp | 17 +++++ src/solvers/conjugate-gradient.cpp | 21 ++++++ src/solvers/incomplete-cholesky.cpp | 13 ++++ src/solvers/incomplete-lut.cpp | 12 ++++ .../least-squares-conjugate-gradient.cpp | 29 ++++++++ src/solvers/minres.cpp | 12 ++++ src/solvers/solvers.cpp | 70 ++++--------------- 8 files changed, 127 insertions(+), 58 deletions(-) create mode 100644 src/solvers/bicgstab.cpp create mode 100644 src/solvers/conjugate-gradient.cpp create mode 100644 src/solvers/incomplete-cholesky.cpp create mode 100644 src/solvers/incomplete-lut.cpp create mode 100644 src/solvers/least-squares-conjugate-gradient.cpp create mode 100644 src/solvers/minres.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f51198127..b08185d35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -318,8 +318,15 @@ list( # ---------------------------------------------------- # --- TARGETS ---------------------------------------- # ---------------------------------------------------- -set(${PROJECT_NAME}_SOLVERS_SOURCES src/solvers/preconditioners.cpp - src/solvers/solvers.cpp) +set(${PROJECT_NAME}_SOLVERS_SOURCES + src/solvers/preconditioners.cpp + src/solvers/solvers.cpp + src/solvers/minres.cpp + src/solvers/bicgstab.cpp + src/solvers/conjugate-gradient.cpp + src/solvers/least-squares-conjugate-gradient.cpp + src/solvers/incomplete-cholesky.cpp + src/solvers/incomplete-lut.cpp) set(${PROJECT_NAME}_DECOMPOSITIONS_SOURCES src/decompositions/decompositions.cpp diff --git a/src/solvers/bicgstab.cpp b/src/solvers/bicgstab.cpp new file mode 100644 index 000000000..f2fb0876f --- /dev/null +++ b/src/solvers/bicgstab.cpp @@ -0,0 +1,17 @@ +/* + * Copyright 2025 INRIA + */ + +#include "eigenpy/solvers/BiCGSTAB.hpp" + +namespace eigenpy { +void exposeBiCGSTAB() { + using namespace Eigen; + using Eigen::BiCGSTAB; + using Eigen::IdentityPreconditioner; + using IdentityBiCGSTAB = BiCGSTAB; + + BiCGSTABVisitor>::expose("BiCGSTAB"); + BiCGSTABVisitor::expose("IdentityBiCGSTAB"); +} +} // namespace eigenpy diff --git a/src/solvers/conjugate-gradient.cpp b/src/solvers/conjugate-gradient.cpp new file mode 100644 index 000000000..a64cce257 --- /dev/null +++ b/src/solvers/conjugate-gradient.cpp @@ -0,0 +1,21 @@ +/* + * Copyright 2025 INRIA + */ + +#include "eigenpy/solvers/ConjugateGradient.hpp" + +namespace eigenpy { +void exposeConjugateGradient() { + using namespace Eigen; + using Eigen::ConjugateGradient; + using Eigen::IdentityPreconditioner; + using Eigen::Lower; + using IdentityConjugateGradient = + ConjugateGradient; + + ConjugateGradientVisitor>::expose( + "ConjugateGradient"); + ConjugateGradientVisitor::expose( + "IdentityConjugateGradient"); +} +} // namespace eigenpy diff --git a/src/solvers/incomplete-cholesky.cpp b/src/solvers/incomplete-cholesky.cpp new file mode 100644 index 000000000..b6b2274de --- /dev/null +++ b/src/solvers/incomplete-cholesky.cpp @@ -0,0 +1,13 @@ +/* + * Copyright 2025 INRIA + */ + +#include "eigenpy/solvers/IncompleteCholesky.hpp" + +namespace eigenpy { +void exposeIncompleteCholesky() { + using namespace Eigen; + typedef SparseMatrix ColMajorSparseMatrix; + IncompleteCholeskyVisitor::expose("IncompleteCholesky"); +} +} // namespace eigenpy diff --git a/src/solvers/incomplete-lut.cpp b/src/solvers/incomplete-lut.cpp new file mode 100644 index 000000000..640fe6178 --- /dev/null +++ b/src/solvers/incomplete-lut.cpp @@ -0,0 +1,12 @@ +/* + * Copyright 2025 INRIA + */ + +#include "eigenpy/solvers/IncompleteLUT.hpp" + +namespace eigenpy { +void exposeIncompleteLUT() { + typedef Eigen::SparseMatrix ColMajorSparseMatrix; + IncompleteLUTVisitor::expose("IncompleteLUT"); +} +} // namespace eigenpy diff --git a/src/solvers/least-squares-conjugate-gradient.cpp b/src/solvers/least-squares-conjugate-gradient.cpp new file mode 100644 index 000000000..5fefbf67a --- /dev/null +++ b/src/solvers/least-squares-conjugate-gradient.cpp @@ -0,0 +1,29 @@ +/* + * Copyright 2025 INRIA + */ + +#include "eigenpy/solvers/LeastSquaresConjugateGradient.hpp" + +namespace eigenpy { +void exposeLeastSquaresConjugateGradient() { + using namespace Eigen; + using Eigen::LeastSquaresConjugateGradient; + + using Eigen::DiagonalPreconditioner; + using Eigen::IdentityPreconditioner; + using Eigen::LeastSquareDiagonalPreconditioner; + + using IdentityLeastSquaresConjugateGradient = + LeastSquaresConjugateGradient; + using DiagonalLeastSquaresConjugateGradient = LeastSquaresConjugateGradient< + MatrixXd, DiagonalPreconditioner>; + + LeastSquaresConjugateGradientVisitor>>:: + expose("LeastSquaresConjugateGradient"); + LeastSquaresConjugateGradientVisitor:: + expose("IdentityLeastSquaresConjugateGradient"); + LeastSquaresConjugateGradientVisitor:: + expose("DiagonalLeastSquaresConjugateGradient"); +} +} // namespace eigenpy diff --git a/src/solvers/minres.cpp b/src/solvers/minres.cpp new file mode 100644 index 000000000..06f8ff63e --- /dev/null +++ b/src/solvers/minres.cpp @@ -0,0 +1,12 @@ +/* + * Copyright 2025 INRIA + */ + +#include "eigenpy/solvers/MINRES.hpp" + +namespace eigenpy { +void exposeMINRES() { + using namespace Eigen; + MINRESSolverVisitor::expose("MINRES"); +} +} // namespace eigenpy diff --git a/src/solvers/solvers.cpp b/src/solvers/solvers.cpp index f1faee195..e9a0d7fd2 100644 --- a/src/solvers/solvers.cpp +++ b/src/solvers/solvers.cpp @@ -2,67 +2,25 @@ * Copyright 2017-2025 CNRS INRIA */ -#include - -#if EIGEN_VERSION_AT_LEAST(3, 2, 0) - -#include "eigenpy/solvers/ConjugateGradient.hpp" #include "eigenpy/solvers/solvers.hpp" -#if EIGEN_VERSION_AT_LEAST(3, 3, 5) -#include "eigenpy/solvers/LeastSquaresConjugateGradient.hpp" -#endif - -#include "eigenpy/solvers/BiCGSTAB.hpp" -#include "eigenpy/solvers/MINRES.hpp" - -#include "eigenpy/solvers/IncompleteLUT.hpp" -#include "eigenpy/solvers/IncompleteCholesky.hpp" +#include "eigenpy/fwd.hpp" namespace eigenpy { -void exposeSolvers() { - using namespace Eigen; - - using Eigen::Lower; - - using Eigen::BiCGSTAB; - using Eigen::ConjugateGradient; - using Eigen::LeastSquaresConjugateGradient; - using Eigen::DiagonalPreconditioner; - using Eigen::IdentityPreconditioner; - using Eigen::LeastSquareDiagonalPreconditioner; +void exposeBiCGSTAB(); +void exposeMINRES(); +void exposeConjugateGradient(); +void exposeLeastSquaresConjugateGradient(); +void exposeIncompleteCholesky(); +void exposeIncompleteLUT(); - using IdentityBiCGSTAB = BiCGSTAB; - using IdentityConjugateGradient = - ConjugateGradient; - using IdentityLeastSquaresConjugateGradient = - LeastSquaresConjugateGradient; - using DiagonalLeastSquaresConjugateGradient = LeastSquaresConjugateGradient< - MatrixXd, DiagonalPreconditioner>; - - ConjugateGradientVisitor>::expose( - "ConjugateGradient"); - ConjugateGradientVisitor::expose( - "IdentityConjugateGradient"); - - LeastSquaresConjugateGradientVisitor>>:: - expose("LeastSquaresConjugateGradient"); - LeastSquaresConjugateGradientVisitor:: - expose("IdentityLeastSquaresConjugateGradient"); - LeastSquaresConjugateGradientVisitor:: - expose("DiagonalLeastSquaresConjugateGradient"); - - BiCGSTABVisitor>::expose("BiCGSTAB"); - BiCGSTABVisitor::expose("IdentityBiCGSTAB"); - - MINRESSolverVisitor::expose("MINRES"); - - typedef SparseMatrix ColMajorSparseMatrix; - IncompleteLUTVisitor::expose("IncompleteLUT"); - IncompleteCholeskyVisitor::expose("IncompleteCholesky"); +void exposeSolvers() { + exposeBiCGSTAB(); + exposeMINRES(); + exposeConjugateGradient(); + exposeLeastSquaresConjugateGradient(); + exposeIncompleteCholesky(); + exposeIncompleteLUT(); } } // namespace eigenpy - -#endif From f6b774b51a7490f4dfa7eb7662178a200a8dcef2 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 24 Jul 2025 18:55:07 +0200 Subject: [PATCH 36/43] fwd:hpp: Corrected definition of macro EIGENPY_PRAGMA_DEPRECATED_HEADER following pinocchio --- include/eigenpy/fwd.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/eigenpy/fwd.hpp b/include/eigenpy/fwd.hpp index c0fe4c75f..d95a81f2c 100644 --- a/include/eigenpy/fwd.hpp +++ b/include/eigenpy/fwd.hpp @@ -36,11 +36,11 @@ #if defined(EIGENPY_CLANG_COMPILER) || defined(EIGENPY_GCC_COMPILER) #define EIGENPY_PRAGMA(x) _Pragma(#x) #define EIGENPY_PRAGMA_MESSAGE(the_message) \ - EIGENPY_PRAGMA(GCC message the_message) + EIGENPY_PRAGMA(GCC message #the_message) #define EIGENPY_PRAGMA_WARNING(the_message) \ - EIGENPY_PRAGMA(GCC warning the_message) + EIGENPY_PRAGMA(GCC warning #the_message) #define EIGENPY_PRAGMA_DEPRECATED(the_message) \ - EIGENPY_PRAGMA_WARNING(Deprecated : the_message) + EIGENPY_PRAGMA_WARNING(Deprecated : #the_message) #define EIGENPY_PRAGMA_DEPRECATED_HEADER(old_header, new_header) \ EIGENPY_PRAGMA_WARNING( \ Deprecated header file : #old_header has been replaced \ From 18275eda6429ef48951e992f97b9ddf02d104c09 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 24 Jul 2025 19:10:19 +0200 Subject: [PATCH 37/43] Add back compatibility headers due to changes in name and directory of headers --- CMakeLists.txt | 10 +++++++--- include/eigenpy/decompositions/minres.hpp | 16 ++++++++++++++++ include/eigenpy/decompositions/sparse/LDLT.hpp | 16 ++++++++++++++++ include/eigenpy/decompositions/sparse/LLT.hpp | 16 ++++++++++++++++ .../decompositions/sparse/SimplicialLDLT.hpp | 6 +++--- .../decompositions/sparse/SimplicialLLT.hpp | 6 +++--- .../sparse/accelerate/accelerate.hpp | 16 ++++++++++++++++ 7 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 include/eigenpy/decompositions/minres.hpp create mode 100644 include/eigenpy/decompositions/sparse/LDLT.hpp create mode 100644 include/eigenpy/decompositions/sparse/LLT.hpp create mode 100644 include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b08185d35..e767907c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -209,7 +209,8 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_CHOLMOD_HEADERS include/eigenpy/decompositions/sparse/cholmod/CholmodSupernodalLLT.hpp) set(${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_ACCELERATE_HEADERS - include/eigenpy/decompositions/sparse/accelerate/Accelerate.hpp) + include/eigenpy/decompositions/sparse/accelerate/Accelerate.hpp + include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp) set(${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_HEADERS include/eigenpy/decompositions/sparse/SimplicialLLT.hpp @@ -217,7 +218,9 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_HEADERS include/eigenpy/decompositions/sparse/SparseLU.hpp include/eigenpy/decompositions/sparse/SparseQR.hpp include/eigenpy/decompositions/sparse/SimplicialCholesky.hpp - include/eigenpy/decompositions/sparse/SparseSolverBase.hpp) + include/eigenpy/decompositions/sparse/SparseSolverBase.hpp + include/eigenpy/decompositions/sparse/LDLT.hpp + include/eigenpy/decompositions/sparse/LLT.hpp) if(BUILD_WITH_CHOLMOD_SUPPORT) list(APPEND ${PROJECT_NAME}_DECOMPOSITIONS_SPARSE_HEADERS @@ -254,7 +257,8 @@ set(${PROJECT_NAME}_DECOMPOSITIONS_HEADERS include/eigenpy/decompositions/SelfAdjointEigenSolver.hpp include/eigenpy/decompositions/SVDBase.hpp include/eigenpy/decompositions/BDCSVD.hpp - include/eigenpy/decompositions/JacobiSVD.hpp) + include/eigenpy/decompositions/JacobiSVD.hpp + include/eigenpy/decompositions/minres.hpp) set(${PROJECT_NAME}_HEADERS ${${PROJECT_NAME}_UTILS_HEADERS} diff --git a/include/eigenpy/decompositions/minres.hpp b/include/eigenpy/decompositions/minres.hpp new file mode 100644 index 000000000..c5bb0dae0 --- /dev/null +++ b/include/eigenpy/decompositions/minres.hpp @@ -0,0 +1,16 @@ +/* + * Copyright 2025 INRIA + */ + +#ifndef __eigenpy_decompositions_minres_hpp__ +#define __eigenpy_decompositions_minres_hpp__ + +#include "eigenpy/fwd.hpp" + +// clang-format off +EIGENPY_PRAGMA_DEPRECATED_HEADER(eigenpy/decompositions/minres.hpp, eigenpy/solvers/MINRES.hpp) +// clang-format on + +#include "eigenpy/solvers/MINRES.hpp" + +#endif // ifndef __eigenpy_decompositions_minres_hpp__ diff --git a/include/eigenpy/decompositions/sparse/LDLT.hpp b/include/eigenpy/decompositions/sparse/LDLT.hpp new file mode 100644 index 000000000..07664e992 --- /dev/null +++ b/include/eigenpy/decompositions/sparse/LDLT.hpp @@ -0,0 +1,16 @@ +/* + * Copyright 2025 INRIA + */ + +#ifndef __eigenpy_decompositions_sparse_ldlt_hpp__ +#define __eigenpy_decompositions_sparse_ldlt_hpp__ + +#include "eigenpy/fwd.hpp" + +// clang-format off +EIGENPY_PRAGMA_DEPRECATED_HEADER(eigenpy/decompositions/sparse/LDLT.hpp, eigenpy/decompositions/sparse/SimplicialLDLT.hpp) +// clang-format on + +#include "eigenpy/decompositions/sparse/SimplicialLDLT.hpp" + +#endif // ifndef __eigenpy_decompositions_sparse_ldlt_hpp__ diff --git a/include/eigenpy/decompositions/sparse/LLT.hpp b/include/eigenpy/decompositions/sparse/LLT.hpp new file mode 100644 index 000000000..795d18e5c --- /dev/null +++ b/include/eigenpy/decompositions/sparse/LLT.hpp @@ -0,0 +1,16 @@ +/* + * Copyright 2025 INRIA + */ + +#ifndef __eigenpy_decompositions_sparse_llt_hpp__ +#define __eigenpy_decompositions_sparse_llt_hpp__ + +#include "eigenpy/fwd.hpp" + +// clang-format off +EIGENPY_PRAGMA_DEPRECATED_HEADER(eigenpy/decompositions/sparse/LLT.hpp, eigenpy/decompositions/sparse/SimplicialLLT.hpp) +// clang-format on + +#include "eigenpy/decompositions/sparse/SimplicialLLT.hpp" + +#endif // ifndef __eigenpy_decompositions_sparse_llt_hpp__ diff --git a/include/eigenpy/decompositions/sparse/SimplicialLDLT.hpp b/include/eigenpy/decompositions/sparse/SimplicialLDLT.hpp index 6c9c39773..3ba78b0fb 100644 --- a/include/eigenpy/decompositions/sparse/SimplicialLDLT.hpp +++ b/include/eigenpy/decompositions/sparse/SimplicialLDLT.hpp @@ -2,8 +2,8 @@ * Copyright 2024 INRIA */ -#ifndef __eigenpy_decompositions_sparse_ldlt_hpp__ -#define __eigenpy_decompositions_sparse_ldlt_hpp__ +#ifndef __eigenpy_decompositions_sparse_simplicial_ldlt_hpp__ +#define __eigenpy_decompositions_sparse_simplicial_ldlt_hpp__ #include "eigenpy/eigenpy.hpp" #include "eigenpy/decompositions/sparse/SimplicialCholesky.hpp" @@ -69,4 +69,4 @@ struct SimplicialLDLTVisitor } // namespace eigenpy -#endif // ifndef __eigenpy_decompositions_sparse_ldlt_hpp__ +#endif // ifndef __eigenpy_decompositions_sparse_simplicial_ldlt_hpp__ diff --git a/include/eigenpy/decompositions/sparse/SimplicialLLT.hpp b/include/eigenpy/decompositions/sparse/SimplicialLLT.hpp index 259f796c1..50581e9cd 100644 --- a/include/eigenpy/decompositions/sparse/SimplicialLLT.hpp +++ b/include/eigenpy/decompositions/sparse/SimplicialLLT.hpp @@ -2,8 +2,8 @@ * Copyright 2024 INRIA */ -#ifndef __eigenpy_decompositions_sparse_llt_hpp__ -#define __eigenpy_decompositions_sparse_llt_hpp__ +#ifndef __eigenpy_decompositions_sparse_simplicial_llt_hpp__ +#define __eigenpy_decompositions_sparse_simplicial_llt_hpp__ #include "eigenpy/eigenpy.hpp" #include "eigenpy/decompositions/sparse/SimplicialCholesky.hpp" @@ -64,4 +64,4 @@ struct SimplicialLLTVisitor } // namespace eigenpy -#endif // ifndef __eigenpy_decompositions_sparse_llt_hpp__ +#endif // ifndef __eigenpy_decompositions_sparse_simplicial_llt_hpp__ diff --git a/include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp b/include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp new file mode 100644 index 000000000..43a4d5765 --- /dev/null +++ b/include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp @@ -0,0 +1,16 @@ +/* + * Copyright 2025 INRIA + */ + +#ifndef __eigenpy_decompositions_sparse_accelerate_accelerate_hpp_deprecated__ +#define __eigenpy_decompositions_sparse_accelerate_accelerate_hpp_deprecated__ + +#include "eigenpy/fwd.hpp" + +// clang-format off +EIGENPY_PRAGMA_DEPRECATED_HEADER(eigenpy/decompositions/sparse/accelerate/accelerate.hpp, eigenpy/decompositions/sparse/accelerate/Accelerate.hpp) +// clang-format on + +#include "eigenpy/decompositions/sparse/accelerate/Accelerate.hpp" + +#endif // ifndef __eigenpy_decompositions_sparse_accelerate_accelerate_hpp_deprecated__ From fca41b2103298e1a7515028222fa5ccde0f606e1 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 24 Jul 2025 19:30:57 +0200 Subject: [PATCH 38/43] unittest: Simplify test_GeneralizedEigenSolver to reduce computation time --- .../python/test_GeneralizedEigenSolver.py | 76 ------------------- 1 file changed, 76 deletions(-) diff --git a/unittest/python/test_GeneralizedEigenSolver.py b/unittest/python/test_GeneralizedEigenSolver.py index 89e2ba416..66748e5fb 100644 --- a/unittest/python/test_GeneralizedEigenSolver.py +++ b/unittest/python/test_GeneralizedEigenSolver.py @@ -8,27 +8,14 @@ B = rng.random((dim, dim)) B = (B + B.T) * 0.5 + np.diag(10.0 + rng.random(dim)) -ges = eigenpy.GeneralizedEigenSolver() -ges_size = eigenpy.GeneralizedEigenSolver(dim) ges_matrices = eigenpy.GeneralizedEigenSolver(A, B) assert ges_matrices.info() == eigenpy.ComputationInfo.Success -ges_with_vectors = eigenpy.GeneralizedEigenSolver(A, B, True) -assert ges_with_vectors.info() == eigenpy.ComputationInfo.Success - -ges_without_vectors = eigenpy.GeneralizedEigenSolver(A, B, False) -assert ges_without_vectors.info() == eigenpy.ComputationInfo.Success - alphas = ges_matrices.alphas() betas = ges_matrices.betas() eigenvectors = ges_matrices.eigenvectors() eigenvalues = ges_matrices.eigenvalues() -assert alphas.shape == (dim,) -assert betas.shape == (dim,) -assert eigenvectors.shape == (dim, dim) -assert eigenvalues.shape == (dim,) - for k in range(dim): v = eigenvectors[:, k] lambda_k = eigenvalues[k] @@ -52,66 +39,3 @@ if abs(betas[k]) > 1e-12: expected_eigenvalue = alphas[k] / betas[k] assert abs(eigenvalues[k] - expected_eigenvalue) < 1e-12 - -ges_compute = eigenpy.GeneralizedEigenSolver() -result = ges_compute.compute(A, B, False) -assert result.info() == eigenpy.ComputationInfo.Success -alphas_only = ges_compute.alphas() -betas_only = ges_compute.betas() -eigenvalues_only = ges_compute.eigenvalues() - -result_with_vectors = ges_compute.compute(A, B, True) -assert result_with_vectors.info() == eigenpy.ComputationInfo.Success -eigenvectors_computed = ges_compute.eigenvectors() - -ges_default = eigenpy.GeneralizedEigenSolver() -result_default = ges_default.compute(A, B) -assert result_default.info() == eigenpy.ComputationInfo.Success - -ges_iter = eigenpy.GeneralizedEigenSolver(A, B) -ges_iter.setMaxIterations(100) -ges_iter.setMaxIterations(200) - -A1 = rng.random((dim, dim)) -B1 = rng.random((dim, dim)) -spdA = A.T @ A + A1.T @ A1 -spdB = B.T @ B + B1.T @ B1 - -ges_spd = eigenpy.GeneralizedEigenSolver(spdA, spdB) -assert ges_spd.info() == eigenpy.ComputationInfo.Success - -spd_eigenvalues = ges_spd.eigenvalues() -max_imag = np.max(np.abs(spd_eigenvalues.imag)) -assert max_imag < 1e-10 - -ges1 = eigenpy.GeneralizedEigenSolver() -ges2 = eigenpy.GeneralizedEigenSolver() -id1 = ges1.id() -id2 = ges2.id() -assert id1 != id2 -assert id1 == ges1.id() -assert id2 == ges2.id() - -ges3 = eigenpy.GeneralizedEigenSolver(dim) -ges4 = eigenpy.GeneralizedEigenSolver(dim) -id3 = ges3.id() -id4 = ges4.id() -assert id3 != id4 -assert id3 == ges3.id() -assert id4 == ges4.id() - -ges5 = eigenpy.GeneralizedEigenSolver(A, B) -ges6 = eigenpy.GeneralizedEigenSolver(A, B) -id5 = ges5.id() -id6 = ges6.id() -assert id5 != id6 -assert id5 == ges5.id() -assert id6 == ges6.id() - -ges7 = eigenpy.GeneralizedEigenSolver(A, B, True) -ges8 = eigenpy.GeneralizedEigenSolver(A, B, False) -id7 = ges7.id() -id8 = ges8.id() -assert id7 != id8 -assert id7 == ges7.id() -assert id8 == ges8.id() From 912c901819e01445dc2c6295a5efaab244d97822 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 24 Jul 2025 19:51:57 +0200 Subject: [PATCH 39/43] Back compatibility accelerate.hpp --- .../eigenpy/decompositions/sparse/accelerate/accelerate.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp b/include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp index 43a4d5765..71ff88b05 100644 --- a/include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp +++ b/include/eigenpy/decompositions/sparse/accelerate/accelerate.hpp @@ -13,4 +13,6 @@ EIGENPY_PRAGMA_DEPRECATED_HEADER(eigenpy/decompositions/sparse/accelerate/accele #include "eigenpy/decompositions/sparse/accelerate/Accelerate.hpp" -#endif // ifndef __eigenpy_decompositions_sparse_accelerate_accelerate_hpp_deprecated__ +#endif + +// ifndef __eigenpy_decompositions_sparse_accelerate_accelerate_hpp_deprecated__ From 0761b74e785fd77b80c0ed5f2a229dfca270dade Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Thu, 24 Jul 2025 19:52:21 +0200 Subject: [PATCH 40/43] unittest: Reduced computation time of tests --- unittest/python/test_ComplexEigenSolver.py | 66 --------------- unittest/python/test_ComplexSchur.py | 68 ---------------- .../test_GeneralizedSelfAdjointEigenSolver.py | 36 --------- unittest/python/test_JacobiSVD.py | 42 +++------- unittest/python/test_RealQZ.py | 61 -------------- unittest/python/test_RealSchur.py | 81 ------------------- 6 files changed, 12 insertions(+), 342 deletions(-) diff --git a/unittest/python/test_ComplexEigenSolver.py b/unittest/python/test_ComplexEigenSolver.py index 433696d05..8157a7221 100644 --- a/unittest/python/test_ComplexEigenSolver.py +++ b/unittest/python/test_ComplexEigenSolver.py @@ -6,17 +6,9 @@ rng = np.random.default_rng() A = rng.random((dim, dim)) -ces = eigenpy.ComplexEigenSolver() -es = eigenpy.ComplexEigenSolver(dim) es = eigenpy.ComplexEigenSolver(A) assert es.info() == eigenpy.ComputationInfo.Success -es_with_vectors = eigenpy.ComplexEigenSolver(A, True) -assert es_with_vectors.info() == eigenpy.ComputationInfo.Success - -es_without_vectors = eigenpy.ComplexEigenSolver(A, False) -assert es_without_vectors.info() == eigenpy.ComputationInfo.Success - V = es.eigenvectors() D = es.eigenvalues() assert V.shape == (dim, dim) @@ -27,56 +19,6 @@ assert eigenpy.is_approx(AV.real, VD.real) assert eigenpy.is_approx(AV.imag, VD.imag) -es_compute = eigenpy.ComplexEigenSolver() -result = es_compute.compute(A, False) -assert result.info() == eigenpy.ComputationInfo.Success -D_only = es_compute.eigenvalues() -assert D_only.shape == (dim,) - -result_with_vectors = es_compute.compute(A, True) -assert result_with_vectors.info() == eigenpy.ComputationInfo.Success -V_computed = es_compute.eigenvectors() -D_computed = es_compute.eigenvalues() -assert V_computed.shape == (dim, dim) -assert D_computed.shape == (dim,) - -trace_A = np.trace(A) -trace_D = np.sum(D) -assert abs(trace_A - trace_D.real) < 1e-10 -assert abs(trace_D.imag) < 1e-10 - -es_default = eigenpy.ComplexEigenSolver() -result_default = es_default.compute(A) -assert result_default.info() == eigenpy.ComputationInfo.Success -V_default = es_default.eigenvectors() -D_default = es_default.eigenvalues() - -es_iter = eigenpy.ComplexEigenSolver(A) -default_iter = es_iter.getMaxIterations() -es_iter.setMaxIterations(100) -assert es_iter.getMaxIterations() == 100 -es_iter.setMaxIterations(200) -assert es_iter.getMaxIterations() == 200 - -assert es.info() == eigenpy.ComputationInfo.Success - -ces1 = eigenpy.ComplexEigenSolver() -ces2 = eigenpy.ComplexEigenSolver() -id1 = ces1.id() -id2 = ces2.id() -assert id1 != id2 -assert id1 == ces1.id() -assert id2 == ces2.id() - -dim_constructor = 3 -ces3 = eigenpy.ComplexEigenSolver(dim_constructor) -ces4 = eigenpy.ComplexEigenSolver(dim_constructor) -id3 = ces3.id() -id4 = ces4.id() -assert id3 != id4 -assert id3 == ces3.id() -assert id4 == ces4.id() - ces5 = eigenpy.ComplexEigenSolver(A) ces6 = eigenpy.ComplexEigenSolver(A) id5 = ces5.id() @@ -84,11 +26,3 @@ assert id5 != id6 assert id5 == ces5.id() assert id6 == ces6.id() - -ces7 = eigenpy.ComplexEigenSolver(A, True) -ces8 = eigenpy.ComplexEigenSolver(A, False) -id7 = ces7.id() -id8 = ces8.id() -assert id7 != id8 -assert id7 == ces7.id() -assert id8 == ces8.id() diff --git a/unittest/python/test_ComplexSchur.py b/unittest/python/test_ComplexSchur.py index 2370a4e22..f777dc930 100644 --- a/unittest/python/test_ComplexSchur.py +++ b/unittest/python/test_ComplexSchur.py @@ -20,50 +20,6 @@ for col in range(row): assert abs(T[row, col]) < 1e-12 -A_test = rng.random((dim, dim)) -cs1 = eigenpy.ComplexSchur(dim) -cs1.compute(A_test) -cs2 = eigenpy.ComplexSchur(A_test) - -assert cs1.info() == eigenpy.ComputationInfo.Success -assert cs2.info() == eigenpy.ComputationInfo.Success - -T1 = cs1.matrixT() -U1 = cs1.matrixU() -T2 = cs2.matrixT() -U2 = cs2.matrixU() - -assert eigenpy.is_approx(T1, T2) -assert eigenpy.is_approx(U1, U2) - -cs_no_u = eigenpy.ComplexSchur(A, False) -assert cs_no_u.info() == eigenpy.ComputationInfo.Success -T_no_u = cs_no_u.matrixT() - -assert eigenpy.is_approx(T, T_no_u) - -cs_compute_no_u = eigenpy.ComplexSchur(dim) -result_no_u = cs_compute_no_u.compute(A, False) -assert result_no_u.info() == eigenpy.ComputationInfo.Success -T_compute_no_u = cs_compute_no_u.matrixT() -assert eigenpy.is_approx(T, T_compute_no_u) - -cs_iter = eigenpy.ComplexSchur(dim) -cs_iter.setMaxIterations(30 * dim) # m_maxIterationsPerRow * size -result_iter = cs_iter.compute(A) -assert result_iter.info() == eigenpy.ComputationInfo.Success -assert cs_iter.getMaxIterations() == 30 * dim - -T_iter = cs_iter.matrixT() -U_iter = cs_iter.matrixU() -assert eigenpy.is_approx(T, T_iter) -assert eigenpy.is_approx(U, U_iter) - -cs_few_iter = eigenpy.ComplexSchur(dim) -cs_few_iter.setMaxIterations(1) -result_few = cs_few_iter.compute(A) -assert cs_few_iter.getMaxIterations() == 1 - A_triangular = np.triu(A) cs_triangular = eigenpy.ComplexSchur(dim) cs_triangular.setMaxIterations(1) @@ -90,27 +46,3 @@ A_complex = A.astype(complex) assert eigenpy.is_approx(A_complex, U_from_hess @ T_from_hess @ U_from_hess.conj().T) - -cs1_id = eigenpy.ComplexSchur(dim) -cs2_id = eigenpy.ComplexSchur(dim) -id1 = cs1_id.id() -id2 = cs2_id.id() -assert id1 != id2 -assert id1 == cs1_id.id() -assert id2 == cs2_id.id() - -cs3_id = eigenpy.ComplexSchur(A) -cs4_id = eigenpy.ComplexSchur(A) -id3 = cs3_id.id() -id4 = cs4_id.id() -assert id3 != id4 -assert id3 == cs3_id.id() -assert id4 == cs4_id.id() - -cs5_id = eigenpy.ComplexSchur(A, True) -cs6_id = eigenpy.ComplexSchur(A, False) -id5 = cs5_id.id() -id6 = cs6_id.id() -assert id5 != id6 -assert id5 == cs5_id.id() -assert id6 == cs6_id.id() diff --git a/unittest/python/test_GeneralizedSelfAdjointEigenSolver.py b/unittest/python/test_GeneralizedSelfAdjointEigenSolver.py index e42c221f9..78ea15564 100644 --- a/unittest/python/test_GeneralizedSelfAdjointEigenSolver.py +++ b/unittest/python/test_GeneralizedSelfAdjointEigenSolver.py @@ -74,42 +74,6 @@ def test_generalized_selfadjoint_eigensolver(options): rank = len([d for d in D if abs(d) > 1e-12]) assert rank <= dim - decomp1 = eigenpy.GeneralizedSelfAdjointEigenSolver() - decomp2 = eigenpy.GeneralizedSelfAdjointEigenSolver() - id1 = decomp1.id() - id2 = decomp2.id() - assert id1 != id2 - assert id1 == decomp1.id() - assert id2 == decomp2.id() - - decomp3 = eigenpy.GeneralizedSelfAdjointEigenSolver(dim) - decomp4 = eigenpy.GeneralizedSelfAdjointEigenSolver(dim) - id3 = decomp3.id() - id4 = decomp4.id() - assert id3 != id4 - assert id3 == decomp3.id() - assert id4 == decomp4.id() - - decomp5 = eigenpy.GeneralizedSelfAdjointEigenSolver(A, B, options) - decomp6 = eigenpy.GeneralizedSelfAdjointEigenSolver(A, B, options) - id5 = decomp5.id() - id6 = decomp6.id() - assert id5 != id6 - assert id5 == decomp5.id() - assert id6 == decomp6.id() - - if compute_eigenvectors and (options & eigenpy.DecompositionOptions.Ax_lBx): - A_pos = A @ A.T + np.eye(dim) - gsaes_pos = eigenpy.GeneralizedSelfAdjointEigenSolver(A_pos, B, options) - assert gsaes_pos.info() == eigenpy.ComputationInfo.Success - - D_pos = gsaes_pos.eigenvalues() - if all(D_pos[i] > 1e-12 for i in range(dim)): - sqrt_matrix = gsaes_pos.operatorSqrt() - inv_sqrt_matrix = gsaes_pos.operatorInverseSqrt() - assert sqrt_matrix.shape == (dim, dim) - assert inv_sqrt_matrix.shape == (dim, dim) - for opt in _options: test_generalized_selfadjoint_eigensolver(opt) diff --git a/unittest/python/test_JacobiSVD.py b/unittest/python/test_JacobiSVD.py index db1981277..a9c975c90 100644 --- a/unittest/python/test_JacobiSVD.py +++ b/unittest/python/test_JacobiSVD.py @@ -9,23 +9,26 @@ _options = [ 0, - THIN_U, - THIN_V, - FULL_U, - FULL_V, + # THIN_U, + # THIN_V, + # FULL_U, + # FULL_V, THIN_U | THIN_V, FULL_U | FULL_V, - THIN_U | FULL_V, - FULL_U | THIN_V, + # THIN_U | FULL_V, + # FULL_U | THIN_V, ] _classes = [ eigenpy.ColPivHhJacobiSVD, - eigenpy.FullPivHhJacobiSVD, - eigenpy.HhJacobiSVD, - eigenpy.NoPrecondJacobiSVD, + # eigenpy.FullPivHhJacobiSVD, + # eigenpy.HhJacobiSVD, + # eigenpy.NoPrecondJacobiSVD, ] +# Rationale: Tets only few cases to gain computation time +# User can test all of them by uncommenting the corresponding lines + def is_valid_combination(cls, opt): if cls == eigenpy.FullPivHhJacobiSVD: @@ -104,27 +107,6 @@ def test_jacobi(cls, opt): A_reconstructed = U @ S_matrix @ V.T assert eigenpy.is_approx(A, A_reconstructed) - jacobisvd.setThreshold() - _default_threshold = jacobisvd.threshold() - jacobisvd.setThreshold(1e-8) - assert jacobisvd.threshold() == 1e-8 - - decomp1 = cls() - decomp2 = cls() - id1 = decomp1.id() - id2 = decomp2.id() - assert id1 != id2 - assert id1 == decomp1.id() - assert id2 == decomp2.id() - - decomp3 = cls(dim, dim, opt) - decomp4 = cls(dim, dim, opt) - id3 = decomp3.id() - id4 = decomp4.id() - assert id3 != id4 - assert id3 == decomp3.id() - assert id4 == decomp4.id() - for cls in _classes: for opt in _options: diff --git a/unittest/python/test_RealQZ.py b/unittest/python/test_RealQZ.py index 30411c408..df6262d43 100644 --- a/unittest/python/test_RealQZ.py +++ b/unittest/python/test_RealQZ.py @@ -29,59 +29,6 @@ for j in range(i - 1): assert abs(S[i, j]) < 1e-12 -realqz_no_qz = eigenpy.RealQZ(A, B, False) -assert realqz_no_qz.info() == eigenpy.ComputationInfo.Success -S_no_qz = realqz_no_qz.matrixS() -T_no_qz = realqz_no_qz.matrixT() - -for i in range(dim): - for j in range(i): - assert abs(T_no_qz[i, j]) < 1e-12 - -for i in range(dim): - for j in range(i - 1): - assert abs(S_no_qz[i, j]) < 1e-12 - -realqz_compute_no_qz = eigenpy.RealQZ(dim) -result_no_qz = realqz_compute_no_qz.compute(A, B, False) -assert result_no_qz.info() == eigenpy.ComputationInfo.Success -S_compute_no_qz = realqz_compute_no_qz.matrixS() -T_compute_no_qz = realqz_compute_no_qz.matrixT() - -realqz_with_qz = eigenpy.RealQZ(dim) -realqz_without_qz = eigenpy.RealQZ(dim) - -result_with = realqz_with_qz.compute(A, B, True) -result_without = realqz_without_qz.compute(A, B, False) - -assert result_with.info() == eigenpy.ComputationInfo.Success -assert result_without.info() == eigenpy.ComputationInfo.Success - -S_with = realqz_with_qz.matrixS() -T_with = realqz_with_qz.matrixT() -S_without = realqz_without_qz.matrixS() -T_without = realqz_without_qz.matrixT() - -assert eigenpy.is_approx(S_with, S_without) -assert eigenpy.is_approx(T_with, T_without) - -iterations = realqz.iterations() -assert iterations >= 0 - -realqz_iter = eigenpy.RealQZ(dim) -realqz_iter.setMaxIterations(100) -realqz_iter.setMaxIterations(500) -result_iter = realqz_iter.compute(A, B) -assert result_iter.info() == eigenpy.ComputationInfo.Success - -realqz1_id = eigenpy.RealQZ(dim) -realqz2_id = eigenpy.RealQZ(dim) -id1 = realqz1_id.id() -id2 = realqz2_id.id() -assert id1 != id2 -assert id1 == realqz1_id.id() -assert id2 == realqz2_id.id() - realqz3_id = eigenpy.RealQZ(A, B) realqz4_id = eigenpy.RealQZ(A, B) id3 = realqz3_id.id() @@ -89,11 +36,3 @@ assert id3 != id4 assert id3 == realqz3_id.id() assert id4 == realqz4_id.id() - -realqz5_id = eigenpy.RealQZ(A, B, True) -realqz6_id = eigenpy.RealQZ(A, B, False) -id5 = realqz5_id.id() -id6 = realqz6_id.id() -assert id5 != id6 -assert id5 == realqz5_id.id() -assert id6 == realqz6_id.id() diff --git a/unittest/python/test_RealSchur.py b/unittest/python/test_RealSchur.py index 78552a286..069d41394 100644 --- a/unittest/python/test_RealSchur.py +++ b/unittest/python/test_RealSchur.py @@ -35,63 +35,6 @@ def verify_is_quasi_triangular(T): verify_is_quasi_triangular(T) -A_test = rng.random((dim, dim)) -rs1 = eigenpy.RealSchur(dim) -rs1.compute(A_test) -rs2 = eigenpy.RealSchur(A_test) - -assert rs1.info() == eigenpy.ComputationInfo.Success -assert rs2.info() == eigenpy.ComputationInfo.Success - -T1 = rs1.matrixT() -U1 = rs1.matrixU() -T2 = rs2.matrixT() -U2 = rs2.matrixU() - -assert eigenpy.is_approx(T1, T2) -assert eigenpy.is_approx(U1, U2) - -rs_no_u = eigenpy.RealSchur(A, False) -assert rs_no_u.info() == eigenpy.ComputationInfo.Success -T_no_u = rs_no_u.matrixT() - -assert eigenpy.is_approx(T, T_no_u) - -rs_compute_no_u = eigenpy.RealSchur(dim) -result_no_u = rs_compute_no_u.compute(A, False) -assert result_no_u.info() == eigenpy.ComputationInfo.Success -T_compute_no_u = rs_compute_no_u.matrixT() -assert eigenpy.is_approx(T, T_compute_no_u) - -rs_iter = eigenpy.RealSchur(dim) -rs_iter.setMaxIterations(40 * dim) # m_maxIterationsPerRow * size -result_iter = rs_iter.compute(A) -assert result_iter.info() == eigenpy.ComputationInfo.Success -assert rs_iter.getMaxIterations() == 40 * dim - -T_iter = rs_iter.matrixT() -U_iter = rs_iter.matrixU() -assert eigenpy.is_approx(T, T_iter) -assert eigenpy.is_approx(U, U_iter) - -if dim > 2: - rs_few_iter = eigenpy.RealSchur(dim) - rs_few_iter.setMaxIterations(1) - result_few = rs_few_iter.compute(A) - assert rs_few_iter.getMaxIterations() == 1 - -A_triangular = np.triu(A) -rs_triangular = eigenpy.RealSchur(dim) -rs_triangular.setMaxIterations(1) -result_triangular = rs_triangular.compute(A_triangular) -assert result_triangular.info() == eigenpy.ComputationInfo.Success - -T_triangular = rs_triangular.matrixT() -U_triangular = rs_triangular.matrixU() - -assert eigenpy.is_approx(T_triangular, A_triangular) -assert eigenpy.is_approx(U_triangular, np.eye(dim)) - hess = eigenpy.HessenbergDecomposition(A) H = hess.matrixH() Q_hess = hess.matrixQ() @@ -104,27 +47,3 @@ def verify_is_quasi_triangular(T): U_from_hess = rs_from_hess.matrixU() assert eigenpy.is_approx(A, U_from_hess @ T_from_hess @ U_from_hess.T) - -rs1_id = eigenpy.RealSchur(dim) -rs2_id = eigenpy.RealSchur(dim) -id1 = rs1_id.id() -id2 = rs2_id.id() -assert id1 != id2 -assert id1 == rs1_id.id() -assert id2 == rs2_id.id() - -rs3_id = eigenpy.RealSchur(A) -rs4_id = eigenpy.RealSchur(A) -id3 = rs3_id.id() -id4 = rs4_id.id() -assert id3 != id4 -assert id3 == rs3_id.id() -assert id4 == rs4_id.id() - -rs5_id = eigenpy.RealSchur(A, True) -rs6_id = eigenpy.RealSchur(A, False) -id5 = rs5_id.id() -id6 = rs6_id.id() -assert id5 != id6 -assert id5 == rs5_id.id() -assert id6 == rs6_id.id() From 9eb1056e17f5abc12203d7f61bc7bf46d1e09082 Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 4 Aug 2025 13:45:58 +0200 Subject: [PATCH 41/43] CHANGELOG: Update --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd14ced6f..a48e365d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added +- Add additional decompositions and solvers from Eigen ([#571](https://github.com/stack-of-tasks/eigenpy/pull/571)) + +### Added + - Docker images `ghcr.io/stack-of-tasks/eigenpy` ([#575](https://github.com/stack-of-tasks/eigenpy/pull/575)) ### Changed From 4dbdc9b1b1129e4670d906b81f336afd7f1e12cc Mon Sep 17 00:00:00 2001 From: Lucas Haubert Date: Mon, 4 Aug 2025 16:45:34 +0200 Subject: [PATCH 42/43] SparseQR: Add toSparse method in matrixQ return type to extract the related Eigen SparseMatrix --- .../decompositions/sparse/SparseQR.hpp | 17 +++++++++++++-- .../decompositions/sparse/test_SparseQR.py | 21 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/include/eigenpy/decompositions/sparse/SparseQR.hpp b/include/eigenpy/decompositions/sparse/SparseQR.hpp index 79145a7f3..e78ca0025 100644 --- a/include/eigenpy/decompositions/sparse/SparseQR.hpp +++ b/include/eigenpy/decompositions/sparse/SparseQR.hpp @@ -64,6 +64,7 @@ struct SparseQRMatrixQReturnTypeVisitor typedef Eigen::SparseQRMatrixQTransposeReturnType QTransposeType; typedef Eigen::SparseQRMatrixQReturnType QType; + typedef typename SparseQRType::QRMatrixType QRMatrixType; typedef Eigen::VectorXd VectorXd; typedef Eigen::MatrixXd MatrixXd; @@ -92,9 +93,21 @@ struct SparseQRMatrixQReturnTypeVisitor +[](const QType& self) -> QTransposeType { return self.adjoint(); }) .def( - "transpose", +[](const QType& self) -> QTransposeType { + "transpose", + +[](const QType& self) -> QTransposeType { return self.transpose(); - }); + }) + + .def( + "toSparse", + +[](QType& self) -> QRMatrixType { + Eigen::Index m = self.rows(); + MatrixXd I = MatrixXd::Identity(m, m); + MatrixXd Q_dense = self * I; + return Q_dense.sparseView(); + }, + bp::arg("self"), + "Convert the Q matrix to a sparse matrix representation."); } static void expose() { diff --git a/unittest/python/decompositions/sparse/test_SparseQR.py b/unittest/python/decompositions/sparse/test_SparseQR.py index 6f86dbfe3..f6b6d070c 100644 --- a/unittest/python/decompositions/sparse/test_SparseQR.py +++ b/unittest/python/decompositions/sparse/test_SparseQR.py @@ -72,3 +72,24 @@ QtAP = Qt @ A_permuted R_dense = spqr.matrixR().toarray() assert eigenpy.is_approx(QtAP, R_dense) + +Q_sparse = Q.toSparse() +R_sparse = R + +assert Q_sparse.shape == (dim, dim) + +QtQ_sparse = Q_sparse.T @ Q_sparse +QQt_sparse = Q_sparse @ Q_sparse.T +I_sparse = spa.identity(dim, format="csc") + +assert eigenpy.is_approx(QtQ_sparse.toarray(), I_sparse.toarray()) +assert eigenpy.is_approx(QQt_sparse.toarray(), I_sparse.toarray()) + +Q_sparse_test_vec = Q_sparse @ test_vec +assert eigenpy.is_approx(Qv, Q_sparse_test_vec) + +Q_sparse_test_matrix = Q_sparse @ test_matrix +assert eigenpy.is_approx(QM, Q_sparse_test_matrix) + +QR_sparse = Q_sparse @ R_sparse.toarray() +assert eigenpy.is_approx(QR_sparse, A_permuted) From e08c704c247314b7d295ad29e453abb382062378 Mon Sep 17 00:00:00 2001 From: Guilhem Saurel Date: Wed, 6 Aug 2025 17:18:14 +0200 Subject: [PATCH 43/43] nix: patch eigen Nixpkgs uses eigen v3.4.0-unstable-2022-04-19, aka https://gitlab.com/libeigen/eigen/-/merge_requests/974, because that is required by onnxruntime. Ref. https://github.com/NixOS/nixpkgs/pull/364362 But that fix actually break other things, which are then fixed in https://gitlab.com/libeigen/eigen/-/merge_requests/977 So we need to apply that here, and hope for the next eigen version --- flake.nix | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/flake.nix b/flake.nix index cd7e7d1f6..a0b48c61c 100644 --- a/flake.nix +++ b/flake.nix @@ -20,20 +20,29 @@ devShells.default = pkgs.mkShell { inputsFrom = [ self'.packages.default ]; }; packages = { default = self'.packages.eigenpy; - eigenpy = pkgs.python3Packages.eigenpy.overrideAttrs (_: { - src = pkgs.lib.fileset.toSource { - root = ./.; - fileset = pkgs.lib.fileset.unions [ - ./CMakeLists.txt - ./doc - ./include - ./package.xml - ./python - ./src - ./unittest - ]; - }; - }); + eigen = pkgs.eigen.overrideAttrs { + # Apply https://gitlab.com/libeigen/eigen/-/merge_requests/977 + postPatch = '' + substituteInPlace Eigen/src/SVD/BDCSVD.h \ + --replace-fail "if (l == 0) {" "if (i >= k && l == 0) {" + ''; + }; + eigenpy = + (pkgs.python3Packages.eigenpy.override { inherit (self'.packages) eigen; }).overrideAttrs + (_: { + src = pkgs.lib.fileset.toSource { + root = ./.; + fileset = pkgs.lib.fileset.unions [ + ./CMakeLists.txt + ./doc + ./include + ./package.xml + ./python + ./src + ./unittest + ]; + }; + }); }; }; };