Skip to content

Commit 01ea160

Browse files
CopilotQuafadasCopilot
authored
Add LU decomposition with partial pivoting (#53)
* Initial plan * Add LU decomposition implementation with comprehensive tests Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> * Address code review comments: fix documentation and remove debug output Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * . * . --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> Co-authored-by: Simon Parten <Quafadas@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Simon Parten <quafadas@gmail.com>
1 parent 59a4a4d commit 01ea160

6 files changed

Lines changed: 613 additions & 4 deletions

File tree

site/docs/cheatsheet.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,15 @@ import narr.*
6767
| Determinant | `m.det` | `np.linalg.det(a)` | `det(a)` |
6868
| Trace | `m.trace` | `np.trace(a)` | `trace(a)` |
6969
| Matrix inverse | `m.inv` | `np.linalg.inv(a)` | `inv(a)` |
70-
| SVD | `val (U, S, Vt) = svd(m)` | `U, S, Vh = np.linalg.svd(a)` | `[U,S,V]=svd(a)` |
70+
| SVD | `val (u, s, v_t) = svd(m)` | `U, S, Vh = np.linalg.svd(a)` | `[U,S,V]=svd(a)` |
7171
| Pseudo-inverse | `pinv(m)` | `np.linalg.pinv(a)` | `pinv(a)` |
7272
| Matrix rank | `m.rank` or `rank(m)` | `np.linalg.matrix_rank(a)` | `rank(a)` |
7373
| Solve linear system | `solve(a,b)` | `np.linalg.solve(a, b)` | `a\b` |
7474
| Eigenvalues/vectors | `eig(m)` | `D, V = np.linalg.eig(a)` | `[V,D]=eig(a)` |
7575
| Cholesky decomposition | `cholesky(m)` | `np.linalg.cholesky(a)` | `chol(a)` |
76-
| QR decomposition | `val (Q, R) = qr(m)` | `Q, R = np.linalg.qr(a)` | `[Q,R]=qr(a,0)` |
77-
| LU decomposition | ??? | `P, L, U = scipy.linalg.lu(a)` | `[L,U,P]=lu(a)` |
76+
| LU decomposition | `val (l, u, p) = lu(m)` | `P, L, U = scipy.linalg.lu(a)` | `[L,U,P]=lu(a)` |
77+
| QR decomposition | `val (q, r) = qr(m)` | `Q, R = np.linalg.qr(a)` | `[Q,R]=qr(a,0)` |
78+
7879

7980
## Reductions and Aggregations
8081

vecxt/src-js-native/lu.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package vecxt
2+
3+
import vecxt.matrix.Matrix
4+
5+
/** LU decomposition placeholder for JS and Native platforms.
6+
*
7+
* Decomposes a matrix A into the product P*A = L*U where:
8+
* - P is a permutation matrix
9+
* - L is a lower triangular matrix with unit diagonal
10+
* - U is an upper triangular matrix
11+
*/
12+
object LU:
13+
14+
/** Computes the LU decomposition with partial pivoting of a matrix.
15+
*
16+
* @param m
17+
* The input matrix to decompose.
18+
* @return
19+
* A named tuple containing:
20+
* - L: Lower triangular matrix with unit diagonal
21+
* - U: Upper triangular matrix
22+
* - P: Permutation array
23+
*/
24+
inline def lu(m: Matrix[Double]): (L: Matrix[Double], U: Matrix[Double], P: Array[Int]) = ???
25+
26+
end LU

vecxt/src-jvm/cholesky.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ object Cholesky:
3232
A
3333
end lowerTriangular
3434

35-
inline def cholesky(m: Matrix[Double])(using inline boundsCheck: BoundsCheck) =
35+
inline def cholesky(m: Matrix[Double])(using inline boundsCheck: BoundsCheck): Matrix[Double] =
3636
nonEmptyMatCheck(m)
3737

3838
symmetricMatCheck(m)

vecxt/src-jvm/lu.scala

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package vecxt
2+
3+
import dev.ludovic.netlib.lapack.JavaLAPACK
4+
import org.netlib.util.intW
5+
import vecxt.matrix.Matrix
6+
import vecxt.MatrixInstance.*
7+
import vecxt.MatrixHelper.zeros
8+
import vecxt.BoundsCheck.BoundsCheck
9+
10+
/** LU decomposition with partial pivoting using LAPACK.
11+
*
12+
* Decomposes a matrix A into the product P*A = L*U where:
13+
* - P is a permutation matrix
14+
* - L is a lower triangular matrix with unit diagonal
15+
* - U is an upper triangular matrix
16+
*/
17+
object LU:
18+
private lazy final val lapack = JavaLAPACK.getInstance()
19+
20+
/** Computes the LU decomposition with partial pivoting of a matrix.
21+
*
22+
* Uses LAPACK's dgetrf routine to compute the factorization P*A = L*U where:
23+
* - P is a permutation matrix (represented as a pivot array)
24+
* - L is a lower triangular matrix with ones on the diagonal
25+
* - U is an upper triangular matrix
26+
*
27+
* The decomposition allows efficient solving of linear systems and computation of determinants.
28+
*
29+
* Note: The factorization is computed even for singular matrices. In such cases, at least one diagonal element of U
30+
* will be zero (or near zero), indicating the matrix is rank-deficient.
31+
*
32+
* @param m
33+
* The input matrix to decompose. Can be square or rectangular (m x n).
34+
* @return
35+
* A named tuple containing:
36+
* - L: Lower triangular matrix with unit diagonal (rows × min(rows,cols))
37+
* - U: Upper triangular matrix (min(rows,cols) × cols)
38+
* - P: Permutation array representing successive row interchanges. At step i, row i was swapped with row P(i). The
39+
* permutation matrix can be reconstructed by applying these swaps in order.
40+
* @throws IllegalArgumentException
41+
* if matrix is empty or if an argument to LAPACK is invalid
42+
*/
43+
inline def lu(m: Matrix[Double])(using
44+
inline bc: BoundsCheck
45+
): (L: Matrix[Double], U: Matrix[Double], P: Array[Int]) =
46+
nonEmptyMatCheck(m)
47+
48+
val rows = m.rows
49+
val cols = m.cols
50+
val minDim = math.min(rows, cols)
51+
52+
// Copy input matrix as LAPACK will overwrite it
53+
val A = m.deepCopy
54+
55+
// Allocate pivot array - LAPACK uses 1-based indexing
56+
val ipiv = Array.ofDim[Int](minDim)
57+
58+
val info = new intW(0)
59+
60+
// Compute LU factorization
61+
lapack.dgetrf(
62+
rows, // M: number of rows
63+
cols, // N: number of columns
64+
A.raw, // A: matrix to factor (overwritten with L and U)
65+
math.max(1, rows), // LDA: leading dimension
66+
ipiv, // IPIV: pivot indices
67+
info // INFO: status
68+
)
69+
70+
// Check for errors
71+
if info.`val` < 0 then
72+
throw new IllegalArgumentException(s"LU decomposition failed: argument ${-info.`val`} had an illegal value")
73+
end if
74+
75+
// Note: info.val > 0 means U(info.val, info.val) is exactly zero
76+
// The factorization has been completed, but U is singular
77+
// We allow this and let the caller handle it
78+
79+
// Extract L and U from the factored matrix
80+
// LAPACK stores L and U in the same matrix:
81+
// - U is in the upper triangle (including diagonal)
82+
// - L is in the lower triangle (diagonal is implicitly 1)
83+
84+
val L = Matrix.zeros[Double](rows, minDim)
85+
val U = Matrix.zeros[Double](minDim, cols)
86+
87+
// Extract L (lower triangle with unit diagonal)
88+
var i = 0
89+
while i < rows do
90+
var j = 0
91+
while j < minDim do
92+
if i > j then
93+
// Below diagonal: copy from A
94+
L(i, j) = A(i, j)
95+
else if i == j then
96+
// On diagonal: set to 1
97+
L(i, j) = 1.0
98+
// else: above diagonal is 0 (already initialized)
99+
end if
100+
j += 1
101+
end while
102+
i += 1
103+
end while
104+
105+
// Extract U (upper triangle)
106+
i = 0
107+
while i < minDim do
108+
var j = 0
109+
while j < cols do
110+
if i <= j then
111+
// On or above diagonal: copy from A
112+
U(i, j) = A(i, j)
113+
// else: below diagonal is 0 (already initialized)
114+
end if
115+
j += 1
116+
end while
117+
i += 1
118+
end while
119+
120+
// Convert LAPACK's 1-based pivot indices to 0-based
121+
val pivotArray = Array.ofDim[Int](minDim)
122+
i = 0
123+
while i < minDim do
124+
pivotArray(i) = ipiv(i) - 1 // Convert from 1-based to 0-based
125+
i += 1
126+
end while
127+
128+
(L = L, U = U, P = pivotArray)
129+
end lu
130+
131+
end LU

vecxt/src/all.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ object all:
2626
export vecxt.Svd.* // JS and native are stubs
2727
export vecxt.Cholesky.* // JS and native are stubs
2828
export vecxt.Eigenvalues.* // JS and native are stubs
29+
export vecxt.LU.* // JS and native are stubs
2930
export vecxt.Solve.* // JS and native are stubs
3031
export vecxt.QR.* // JS and native are stubs
3132
// Random

0 commit comments

Comments
 (0)