Skip to content

Add LU decomposition with partial pivoting#735

Merged
yungyuc merged 1 commit into
solvcon:masterfrom
tigercosmos:linear_solver_0412
Apr 26, 2026
Merged

Add LU decomposition with partial pivoting#735
yungyuc merged 1 commit into
solvcon:masterfrom
tigercosmos:linear_solver_0412

Conversation

@tigercosmos
Copy link
Copy Markdown
Collaborator

@tigercosmos tigercosmos commented Apr 18, 2026

Summary

  • Implement LU factorization with partial pivoting (PA=LU) for general (non-symmetric) matrices in cpp/modmesh/linalg/lu_factorization.hpp
  • Add lu_factorization(), lu_solve(), and lu_inv() free functions with Python bindings for float32, float64, complex64, and complex128 types
  • Add .solve(b) and .inv() convenience methods on SimpleArray for floating-point and complex types (integer arrays excluded)

Test plan

  • test_lu_solve_and_inv_dtypes: parametric test across all 4 supported dtypes (float64, float32, complex128, complex64) verifying solve and inverse
  • test_lu_solve_2d_rhs: multi-RHS solve with 5×5 matrix and 5×4 RHS (n,m both > 3)
  • test_lu_singular: singular matrix raises RuntimeError
  • test_lu_invalid_inputs: non-square, 1D, dimension mismatch, wrong rank all raise ValueError
  • test_simplearray_int_no_solve_inv: integer SimpleArray does not expose solve/inv
  • All 35 tests pass (pytest tests/test_linalg.py)

Document

Please also check the Markdown file (gist) for detailed explanation of the math and code.

The PR is part of #719.

@tigercosmos tigercosmos force-pushed the linear_solver_0412 branch 3 times, most recently from ea0d54f to bc6585d Compare April 18, 2026 18:42
Comment on lines +47 to +65
/**
* LU decomposition with partial pivoting for general (non-symmetric) matrices.
*
* Given an n-by-n matrix A, this class computes the factorization PA = LU,
* where:
* - P is a permutation matrix (represented as a pivot index vector),
* - L is a unit lower triangular matrix (diagonal elements are implicitly 1),
* - U is an upper triangular matrix.
*
* L and U are stored compactly in a single n-by-n array: the strictly lower
* triangle holds L (excluding the unit diagonal), and the upper triangle
* (including the diagonal) holds U.
*
* The pivot vector piv[k] records that row k was swapped with row piv[k]
* during the k-th elimination step. Swaps are applied sequentially from
* k = 0 to k = n-1.
*
* Supported element types: float, double, Complex<float>, Complex<double>.
*/
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to make the comment clear. The PR description also includes a HTML explanation file. Almost all code are generated by AI, but I have checked all logic.

* @throws std::invalid_argument if a is not a square 2D array.
* @throws std::runtime_error if the matrix is singular or near-singular.
*/
static std::pair<array_type, std::vector<ssize_t>> factorize(array_type const & a);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meat.

* dimensions are incompatible.
* @throws std::runtime_error if a is singular.
*/
static array_type solve(array_type const & a, array_type const & b);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solver's entry.

Comment on lines +436 to +440
template <typename T>
SimpleArray<T> lu_solve(SimpleArray<T> const & a, SimpleArray<T> const & b)
{
return detail::Lu<T>::solve(a, b);
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Export the free function.

Comment thread tests/test_linalg.py Outdated
Comment on lines +765 to +771
cases = [
# (np_dtype, SimpleArray class, matrix size, rtol, atol)
(np.float64, mm.SimpleArrayFloat64, 4, 1e-10, 1e-10),
(np.float32, mm.SimpleArrayFloat32, 3, 1e-5, 1e-5),
(np.complex128, mm.SimpleArrayComplex128, 3, 1e-10, 1e-10),
(np.complex64, mm.SimpleArrayComplex64, 2, 1e-5, 1e-5),
]
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to cover different dtypes and matrix sizes.

Comment thread tests/test_linalg.py Outdated
np.testing.assert_allclose(ps_upd, ps_upd_np, atol=1e-12, rtol=0.0)


class TestLuSolver(unittest.TestCase):
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be more test cases. I intended keep the most important ones to keep the PR small.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, just add more tests without worrying about the diff length. The tests are necessary.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, added more test cases.

@tigercosmos
Copy link
Copy Markdown
Collaborator Author

@yungyuc Please take a look. The PR slightly exceeds 500 LOC, but some of the code are just document comments.

Copy link
Copy Markdown
Member

@yungyuc yungyuc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Points to address:

  • It's OK to add more testing code even though there are many lines already. The testing code is the key to the work.
  • Use explicit test fixture.
  • Clarify why testing for the include file existence.

Comment thread tests/test_linalg.py Outdated
np.testing.assert_allclose(ps_upd, ps_upd_np, atol=1e-12, rtol=0.0)


class TestLuSolver(unittest.TestCase):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, just add more tests without worrying about the diff length. The tests are necessary.

#include <modmesh/buffer/pymod/buffer_pymod.hpp> // Must be the first include.

#include <modmesh/buffer/pymod/array_common.hpp>
#if __has_include(<modmesh/linalg/lu_factorization.hpp>)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to test for the existence of the include file? In what scenario it would not exist?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Remove it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you meant that it is not necessary.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thank you.

@yungyuc yungyuc added the array Multi-dimensional array implementation label Apr 19, 2026
@yungyuc yungyuc moved this from Todo to In Progress in tensor operations Apr 19, 2026
@yungyuc
Copy link
Copy Markdown
Member

yungyuc commented Apr 19, 2026

Document

Please also check the HTML file (gist) for detailed explanation of the math and code.

It's not easy to read the raw HTML. Gist does not seem to allow rendering it.

image

Using markdown with mathjax (e.g., $$\mathrm{A} x = b$$) would be easier to read, but it may take additional time to write. We can discuss what is a better approach.

@tigercosmos
Copy link
Copy Markdown
Collaborator Author

Using markdown with mathjax would be easier to read, but it may take additional time to write. We can discuss what is a better approach.

I think gist will only show the raw file anyway? Maybe I am wrong.
I choose HTML files because it can have the most flexibility on visualization, such as charts, colors or even animations.

In this case, I would suggest you download the html file, and use a browser to open it.

@yungyuc
Copy link
Copy Markdown
Member

yungyuc commented Apr 19, 2026

Gist render mathjax in markdown. See https://gist.github.com/yungyuc/3f5fd600c91a536d881c581a2045eb06 for an example.

@tigercosmos
Copy link
Copy Markdown
Collaborator Author

@yungyuc Thanks, I will try using markdown next time.

@yungyuc
Copy link
Copy Markdown
Member

yungyuc commented Apr 19, 2026

No problem. Could you rewrite it in markdown this time? It is very inconvenient to download and review HTML files. It is also insecure.

@tigercosmos
Copy link
Copy Markdown
Collaborator Author

No problem. Could you rewrite it in markdown this time? It is very inconvenient to download and review HTML files. It is also insecure.

Sure, I have update the gist to Markdown.

@tigercosmos tigercosmos marked this pull request as draft April 19, 2026 10:24
@tigercosmos tigercosmos force-pushed the linear_solver_0412 branch 4 times, most recently from 83762a9 to e758716 Compare April 19, 2026 12:19
@tigercosmos tigercosmos marked this pull request as ready for review April 19, 2026 12:24
@tigercosmos
Copy link
Copy Markdown
Collaborator Author

@yungyuc I have added more test cases in fixtures. Please take a look, thanks!

Copy link
Copy Markdown
Member

@yungyuc yungyuc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A point to address in the engine:

  • Lu::format_shape() is long enough to be put outside the class declaration of Lu.

Points to address for testing:

  • Always add dtype when creating ndarray.
  • Clarify what does "complex-path" mean.
  • Make sure the factorization error out correctly when the system has a very small eigenvalue (nearly singular).

Comment thread tests/test_linalg.py Outdated
[2.0, 1.0, 1.0],
[4.0, -6.0, 0.0],
[-2.0, 7.0, 2.0],
])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always add dtype='string' when creating ndarray. The explicit dtype specification makes maintenance and grepping easier.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, add dtype for all arrays.

Comment thread tests/test_linalg.py Outdated
[1.0, 1.0, 1.0],
[0.0, 1.0, 2.0],
])
# 3x3 complex matrix for complex-path coverage.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "complex-path" mean? Please provide a reference in the PR comment.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified the comment. It should meant testing Lu<Complex<double>>.

Comment thread tests/test_linalg.py
ValueError, r"must be a square 2D SimpleArray"):
mm.lu_factorization(A_1d)

def test_factorize_rejects_singular_duplicate_row(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's important to test for singular system. Duplicated rows or columns make the system singular, but very small eigenvalues also make the system almost singular.

Please add a test for tiny eigenvalue.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a new test_factorize_rejects_near_singular_tiny_eigenvalue.

#include <modmesh/buffer/pymod/buffer_pymod.hpp> // Must be the first include.

#include <modmesh/buffer/pymod/array_common.hpp>
#if __has_include(<modmesh/linalg/lu_factorization.hpp>)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you meant that it is not necessary.

Comment thread cpp/modmesh/linalg/lu_factorization.hpp Outdated
using array_type = SimpleArray<value_type>;

// Helper: format a SimpleArray shape as "(d0, d1, ...)" for error messages.
static std::string format_shape(array_type const & arr)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lu::format_shape() is long enough to be put outside the class declaration of Lu. But if it is intentional to put it here, it's short enough to be OK.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified.

Comment thread cpp/modmesh/linalg/lu_factorization.hpp Outdated
{

static_assert(
std::is_floating_point_v<T> || is_complex_v<T>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future we can consider to implement is_real_v by making it an alias to std::is_floating_point_v. Then the error message can simply write "... a real or complex number type".

Copy link
Copy Markdown
Collaborator Author

@tigercosmos tigercosmos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yungyuc Please take a look. Thanks!

constexpr bool is_complex_v = is_complex<T>::value;

template <typename T>
constexpr bool is_real_v = is_real<T>::value;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added is_real_v.

Comment thread tests/test_linalg.py
[1.0, 1.0, 1.0],
[0.0, 1.0, 2.0],
], dtype=np.float64)
# 3x3 complex matrix to exercise Lu<T> instantiated for
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the comment.

Comment thread tests/test_linalg.py
RuntimeError, r"singular or near-singular"):
mm.lu_factorization(A)

def test_factorize_rejects_near_singular_tiny_eigenvalue(self):
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New added test case for the small eigenvalue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition.

Comment thread tests/test_linalg.py Outdated
[5.0, 6.0, 8.0, 8.0],
[9.0, 10.0, 11.0, 16.0],
[13.0, 15.0, 16.0, 17.0],
], dtype=np.float64)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always added dtype.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will help reader to always know what data type the array uses.

Please use the string version dtype='float64' (double quotes is OK if you like: dtype="float64") for fundamental types.

In terms of functionality it does not matter to use string or object for dtype. It's just for style consistency.

Comment on lines +231 to +234
// Absolute threshold (~100 * machine eps); works for well-scaled inputs.
// TODO: make it relative to matrix/column magnitude for better robustness.
real_type const singular_tol = real_type(100) * eps;
if (pivot <= singular_tol)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more changed compared with the previous review.

Copy link
Copy Markdown
Member

@yungyuc yungyuc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only a minor formatting issue remains. If you want to fix it in the future, I can merge now:

  • Use the string version for dtype of fundamental types, e.g., dtype='float64' (double quotes is OK if you like: dtype="float64") for fundamental types.

Comment thread tests/test_linalg.py
RuntimeError, r"singular or near-singular"):
mm.lu_factorization(A)

def test_factorize_rejects_near_singular_tiny_eigenvalue(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition.

Comment thread tests/test_linalg.py Outdated
[5.0, 6.0, 8.0, 8.0],
[9.0, 10.0, 11.0, 16.0],
[13.0, 15.0, 16.0, 17.0],
], dtype=np.float64)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will help reader to always know what data type the array uses.

Please use the string version dtype='float64' (double quotes is OK if you like: dtype="float64") for fundamental types.

In terms of functionality it does not matter to use string or object for dtype. It's just for style consistency.

Implement LU factorization (PA=LU), linear system solve (Ax=b), and
matrix inverse (A^-1) for float, double, complex64, and complex128
types.  Expose lu_factorization, lu_solve, lu_inv as free functions.

The .solve() and .inv() convenience methods on SimpleArray (for
floating-point and complex types only) are attached from the linalg
module via pybind11, so the buffer module has no dependency on
linalg/.  Tests cover factorization reconstruction (PA = LU) with
explicit fixtures, known-solution solve/inverse checks, pivoting
behavior, complex types, and error paths (non-square, 1D input,
dimension mismatch, singular matrix).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tigercosmos
Copy link
Copy Markdown
Collaborator Author

@yungyuc I have fixed the formating issue.

Copy link
Copy Markdown
Member

@yungyuc yungyuc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@yungyuc yungyuc merged commit 3d73a5d into solvcon:master Apr 26, 2026
14 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in tensor operations Apr 26, 2026
@tigercosmos tigercosmos deleted the linear_solver_0412 branch April 26, 2026 06:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

array Multi-dimensional array implementation

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants