Add LU decomposition with partial pivoting#735
Conversation
ea0d54f to
bc6585d
Compare
| /** | ||
| * 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>. | ||
| */ |
There was a problem hiding this comment.
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); |
| * dimensions are incompatible. | ||
| * @throws std::runtime_error if a is singular. | ||
| */ | ||
| static array_type solve(array_type const & a, array_type const & b); |
There was a problem hiding this comment.
Solver's entry.
| template <typename T> | ||
| SimpleArray<T> lu_solve(SimpleArray<T> const & a, SimpleArray<T> const & b) | ||
| { | ||
| return detail::Lu<T>::solve(a, b); | ||
| } |
There was a problem hiding this comment.
Export the free function.
| 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), | ||
| ] |
There was a problem hiding this comment.
Try to cover different dtypes and matrix sizes.
| np.testing.assert_allclose(ps_upd, ps_upd_np, atol=1e-12, rtol=0.0) | ||
|
|
||
|
|
||
| class TestLuSolver(unittest.TestCase): |
There was a problem hiding this comment.
There could be more test cases. I intended keep the most important ones to keep the PR small.
There was a problem hiding this comment.
In this case, just add more tests without worrying about the diff length. The tests are necessary.
There was a problem hiding this comment.
Sure, added more test cases.
bc6585d to
8909a40
Compare
|
@yungyuc Please take a look. The PR slightly exceeds 500 LOC, but some of the code are just document comments. |
9f38505 to
862f097
Compare
| np.testing.assert_allclose(ps_upd, ps_upd_np, atol=1e-12, rtol=0.0) | ||
|
|
||
|
|
||
| class TestLuSolver(unittest.TestCase): |
There was a problem hiding this comment.
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>) |
There was a problem hiding this comment.
Why do you need to test for the existence of the include file? In what scenario it would not exist?
There was a problem hiding this comment.
Indeed. Remove it.
There was a problem hiding this comment.
I guess you meant that it is not necessary.
There was a problem hiding this comment.
Yes, thank you.
It's not easy to read the raw HTML. Gist does not seem to allow rendering it.
Using markdown with mathjax (e.g., |
I think gist will only show the raw file anyway? Maybe I am wrong. In this case, I would suggest you download the html file, and use a browser to open it. |
|
Gist render mathjax in markdown. See https://gist.github.com/yungyuc/3f5fd600c91a536d881c581a2045eb06 for an example. |
|
@yungyuc Thanks, I will try using markdown next time. |
|
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. |
83762a9 to
e758716
Compare
|
@yungyuc I have added more test cases in fixtures. Please take a look, thanks! |
There was a problem hiding this comment.
A point to address in the engine:
-
Lu::format_shape()is long enough to be put outside the class declaration ofLu.
Points to address for testing:
- Always add
dtypewhen creatingndarray. - Clarify what does "complex-path" mean.
- Make sure the factorization error out correctly when the system has a very small eigenvalue (nearly singular).
| [2.0, 1.0, 1.0], | ||
| [4.0, -6.0, 0.0], | ||
| [-2.0, 7.0, 2.0], | ||
| ]) |
There was a problem hiding this comment.
Always add dtype='string' when creating ndarray. The explicit dtype specification makes maintenance and grepping easier.
There was a problem hiding this comment.
Fixed, add dtype for all arrays.
| [1.0, 1.0, 1.0], | ||
| [0.0, 1.0, 2.0], | ||
| ]) | ||
| # 3x3 complex matrix for complex-path coverage. |
There was a problem hiding this comment.
What does "complex-path" mean? Please provide a reference in the PR comment.
There was a problem hiding this comment.
Modified the comment. It should meant testing Lu<Complex<double>>.
| ValueError, r"must be a square 2D SimpleArray"): | ||
| mm.lu_factorization(A_1d) | ||
|
|
||
| def test_factorize_rejects_singular_duplicate_row(self): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>) |
There was a problem hiding this comment.
I guess you meant that it is not necessary.
| 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) |
There was a problem hiding this comment.
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.
| { | ||
|
|
||
| static_assert( | ||
| std::is_floating_point_v<T> || is_complex_v<T>, |
There was a problem hiding this comment.
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".
e758716 to
7eba43d
Compare
7eba43d to
c8ced39
Compare
tigercosmos
left a comment
There was a problem hiding this comment.
@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; |
There was a problem hiding this comment.
Added is_real_v.
| [1.0, 1.0, 1.0], | ||
| [0.0, 1.0, 2.0], | ||
| ], dtype=np.float64) | ||
| # 3x3 complex matrix to exercise Lu<T> instantiated for |
There was a problem hiding this comment.
Fixed the comment.
| RuntimeError, r"singular or near-singular"): | ||
| mm.lu_factorization(A) | ||
|
|
||
| def test_factorize_rejects_near_singular_tiny_eigenvalue(self): |
There was a problem hiding this comment.
New added test case for the small eigenvalue.
| [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) |
There was a problem hiding this comment.
Always added dtype.
There was a problem hiding this comment.
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.
| // 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) |
There was a problem hiding this comment.
One more changed compared with the previous review.
| RuntimeError, r"singular or near-singular"): | ||
| mm.lu_factorization(A) | ||
|
|
||
| def test_factorize_rejects_near_singular_tiny_eigenvalue(self): |
| [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) |
There was a problem hiding this comment.
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>
c8ced39 to
0eb0621
Compare
|
@yungyuc I have fixed the formating issue. |

Summary
cpp/modmesh/linalg/lu_factorization.hpplu_factorization(),lu_solve(), andlu_inv()free functions with Python bindings for float32, float64, complex64, and complex128 types.solve(b)and.inv()convenience methods onSimpleArrayfor 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 inversetest_lu_solve_2d_rhs: multi-RHS solve with 5×5 matrix and 5×4 RHS (n,m both > 3)test_lu_singular: singular matrix raisesRuntimeErrortest_lu_invalid_inputs: non-square, 1D, dimension mismatch, wrong rank all raiseValueErrortest_simplearray_int_no_solve_inv: integer SimpleArray does not expose solve/invpytest 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.