2023-04-01 05:11:24 +00:00
|
|
|
|
"""
|
|
|
|
|
Lower–upper (LU) decomposition factors a matrix as a product of a lower
|
|
|
|
|
triangular matrix and an upper triangular matrix. A square matrix has an LU
|
|
|
|
|
decomposition under the following conditions:
|
|
|
|
|
- If the matrix is invertible, then it has an LU decomposition if and only
|
|
|
|
|
if all of its leading principal minors are non-zero (see
|
|
|
|
|
https://en.wikipedia.org/wiki/Minor_(linear_algebra) for an explanation of
|
|
|
|
|
leading principal minors of a matrix).
|
|
|
|
|
- If the matrix is singular (i.e., not invertible) and it has a rank of k
|
|
|
|
|
(i.e., it has k linearly independent columns), then it has an LU
|
|
|
|
|
decomposition if its first k leading principal minors are non-zero.
|
|
|
|
|
|
|
|
|
|
This algorithm will simply attempt to perform LU decomposition on any square
|
|
|
|
|
matrix and raise an error if no such decomposition exists.
|
2019-07-01 08:10:18 +00:00
|
|
|
|
|
2023-04-01 05:11:24 +00:00
|
|
|
|
Reference: https://en.wikipedia.org/wiki/LU_decomposition
|
2020-12-23 09:52:43 +00:00
|
|
|
|
"""
|
2021-09-07 11:37:03 +00:00
|
|
|
|
from __future__ import annotations
|
2018-10-19 12:48:28 +00:00
|
|
|
|
|
2020-12-23 09:52:43 +00:00
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
2023-04-01 05:11:24 +00:00
|
|
|
|
def lower_upper_decomposition(table: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
|
|
|
"""
|
|
|
|
|
Perform LU decomposition on a given matrix and raises an error if the matrix
|
|
|
|
|
isn't square or if no such decomposition exists
|
2020-12-23 09:52:43 +00:00
|
|
|
|
>>> matrix = np.array([[2, -2, 1], [0, 1, 2], [5, 3, 1]])
|
2023-04-01 05:11:24 +00:00
|
|
|
|
>>> lower_mat, upper_mat = lower_upper_decomposition(matrix)
|
|
|
|
|
>>> lower_mat
|
2020-12-23 09:52:43 +00:00
|
|
|
|
array([[1. , 0. , 0. ],
|
|
|
|
|
[0. , 1. , 0. ],
|
|
|
|
|
[2.5, 8. , 1. ]])
|
2023-04-01 05:11:24 +00:00
|
|
|
|
>>> upper_mat
|
2020-12-23 09:52:43 +00:00
|
|
|
|
array([[ 2. , -2. , 1. ],
|
|
|
|
|
[ 0. , 1. , 2. ],
|
|
|
|
|
[ 0. , 0. , -17.5]])
|
|
|
|
|
|
2023-04-01 05:11:24 +00:00
|
|
|
|
>>> matrix = np.array([[4, 3], [6, 3]])
|
|
|
|
|
>>> lower_mat, upper_mat = lower_upper_decomposition(matrix)
|
|
|
|
|
>>> lower_mat
|
|
|
|
|
array([[1. , 0. ],
|
|
|
|
|
[1.5, 1. ]])
|
|
|
|
|
>>> upper_mat
|
|
|
|
|
array([[ 4. , 3. ],
|
|
|
|
|
[ 0. , -1.5]])
|
|
|
|
|
|
|
|
|
|
# Matrix is not square
|
2020-12-23 09:52:43 +00:00
|
|
|
|
>>> matrix = np.array([[2, -2, 1], [0, 1, 2]])
|
2023-04-01 05:11:24 +00:00
|
|
|
|
>>> lower_mat, upper_mat = lower_upper_decomposition(matrix)
|
2020-12-23 09:52:43 +00:00
|
|
|
|
Traceback (most recent call last):
|
2022-10-27 17:42:30 +00:00
|
|
|
|
...
|
2020-12-23 09:52:43 +00:00
|
|
|
|
ValueError: 'table' has to be of square shaped array but got a 2x3 array:
|
|
|
|
|
[[ 2 -2 1]
|
|
|
|
|
[ 0 1 2]]
|
2023-04-01 05:11:24 +00:00
|
|
|
|
|
|
|
|
|
# Matrix is invertible, but its first leading principal minor is 0
|
|
|
|
|
>>> matrix = np.array([[0, 1], [1, 0]])
|
|
|
|
|
>>> lower_mat, upper_mat = lower_upper_decomposition(matrix)
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
...
|
|
|
|
|
ArithmeticError: No LU decomposition exists
|
|
|
|
|
|
|
|
|
|
# Matrix is singular, but its first leading principal minor is 1
|
|
|
|
|
>>> matrix = np.array([[1, 0], [1, 0]])
|
|
|
|
|
>>> lower_mat, upper_mat = lower_upper_decomposition(matrix)
|
|
|
|
|
>>> lower_mat
|
|
|
|
|
array([[1., 0.],
|
|
|
|
|
[1., 1.]])
|
|
|
|
|
>>> upper_mat
|
|
|
|
|
array([[1., 0.],
|
|
|
|
|
[0., 0.]])
|
|
|
|
|
|
|
|
|
|
# Matrix is singular, but its first leading principal minor is 0
|
|
|
|
|
>>> matrix = np.array([[0, 1], [0, 1]])
|
|
|
|
|
>>> lower_mat, upper_mat = lower_upper_decomposition(matrix)
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
...
|
|
|
|
|
ArithmeticError: No LU decomposition exists
|
2020-12-23 09:52:43 +00:00
|
|
|
|
"""
|
2023-04-01 05:11:24 +00:00
|
|
|
|
# Ensure that table is a square array
|
2020-12-23 09:52:43 +00:00
|
|
|
|
rows, columns = np.shape(table)
|
2019-07-01 08:10:18 +00:00
|
|
|
|
if rows != columns:
|
2023-05-26 07:34:17 +00:00
|
|
|
|
msg = (
|
|
|
|
|
"'table' has to be of square shaped array but got a "
|
2023-04-01 05:11:24 +00:00
|
|
|
|
f"{rows}x{columns} array:\n{table}"
|
2020-12-23 09:52:43 +00:00
|
|
|
|
)
|
2023-05-26 07:34:17 +00:00
|
|
|
|
raise ValueError(msg)
|
2023-04-01 05:11:24 +00:00
|
|
|
|
|
2020-12-23 09:52:43 +00:00
|
|
|
|
lower = np.zeros((rows, columns))
|
|
|
|
|
upper = np.zeros((rows, columns))
|
2023-10-16 07:29:46 +00:00
|
|
|
|
|
|
|
|
|
# in 'total', the necessary data is extracted through slices
|
|
|
|
|
# and the sum of the products is obtained.
|
|
|
|
|
|
2019-07-01 08:10:18 +00:00
|
|
|
|
for i in range(columns):
|
2020-08-01 20:11:39 +00:00
|
|
|
|
for j in range(i):
|
2023-10-16 07:29:46 +00:00
|
|
|
|
total = np.sum(lower[i, :i] * upper[:i, j])
|
2023-04-01 05:11:24 +00:00
|
|
|
|
if upper[j][j] == 0:
|
|
|
|
|
raise ArithmeticError("No LU decomposition exists")
|
2020-12-23 09:52:43 +00:00
|
|
|
|
lower[i][j] = (table[i][j] - total) / upper[j][j]
|
|
|
|
|
lower[i][i] = 1
|
2020-08-01 20:11:39 +00:00
|
|
|
|
for j in range(i, columns):
|
2023-10-16 07:29:46 +00:00
|
|
|
|
total = np.sum(lower[i, :i] * upper[:i, j])
|
2020-12-23 09:52:43 +00:00
|
|
|
|
upper[i][j] = table[i][j] - total
|
|
|
|
|
return lower, upper
|
2019-07-01 08:10:18 +00:00
|
|
|
|
|
2018-10-19 12:48:28 +00:00
|
|
|
|
|
2018-11-05 17:19:08 +00:00
|
|
|
|
if __name__ == "__main__":
|
2020-12-23 09:52:43 +00:00
|
|
|
|
import doctest
|
|
|
|
|
|
|
|
|
|
doctest.testmod()
|