diff --git a/benchmarks/gaussian_elimination_and_lu_decomposition_benchmark.py b/benchmarks/gaussian_elimination_and_lu_decomposition_benchmark.py
new file mode 100644
index 0000000..c5ad086
--- /dev/null
+++ b/benchmarks/gaussian_elimination_and_lu_decomposition_benchmark.py
@@ -0,0 +1,184 @@
+from __future__ import annotations
+
+import argparse
+import os
+import random
+import time
+from collections.abc import Callable
+
+import matplotlib.pyplot as plt
+
+from matrix_calculus.lu_decomposition import LUDecomposition
+from matrix_calculus.matrix_processor import MatrixProcessor
+
+
+def generate_matrix(n: int, seed: int = 0) -> list[list[float]]:
+ random.seed(seed)
+
+ A = [[random.uniform(-1, 1) for _ in range(n)] for _ in range(n)]
+
+ for i in range(n):
+ A[i][i] += n # stabilność
+
+ return A
+
+
+def measure_time(func: Callable, *args) -> float:
+ start = time.perf_counter()
+ func(*args)
+ end = time.perf_counter()
+ return end - start
+
+
+def benchmark_gaussian_elimination(out_dir: str, n_max: int):
+ sizes = list(range(10, n_max + 1, 10))
+
+ times_no_pivot = []
+ times_pivot = []
+
+ for n in sizes:
+ M = generate_matrix(n, seed=42)
+
+ t1 = measure_time(MatrixProcessor.gaussian_elimination, M, False)
+ t2 = measure_time(MatrixProcessor.gaussian_elimination, M, True)
+
+ times_no_pivot.append(t1)
+ times_pivot.append(t2)
+
+ print(f"[Gauss] n={n} done")
+
+ path = os.path.join(out_dir, "gauss_benchmark.png")
+
+ plt.figure()
+ plt.plot(sizes, times_no_pivot, label="bez pivotingu")
+ plt.plot(sizes, times_pivot, label="z pivotingiem")
+ plt.xlabel("Rozmiar macierzy (n)")
+ plt.ylabel("Czas [s]")
+ plt.title("Eliminacja Gaussa – porównanie")
+ plt.legend()
+ plt.grid()
+ plt.savefig(path)
+
+ print(f"[SAVE] {path}")
+
+
+def benchmark_lu_decomposition(out_dir: str, n_max: int):
+ sizes = list(range(10, n_max + 1, 10))
+
+ times_no_pivot = []
+ times_pivot = []
+
+ for n in sizes:
+ M = generate_matrix(n, seed=123)
+
+ t1 = measure_time(LUDecomposition.decompose, M, False)
+ t2 = measure_time(LUDecomposition.decompose, M, True)
+
+ times_no_pivot.append(t1)
+ times_pivot.append(t2)
+
+ print(f"[LU] n={n} done")
+
+ path = os.path.join(out_dir, "lu_benchmark.png")
+
+ plt.figure()
+ plt.plot(sizes, times_no_pivot, label="bez pivotingu")
+ plt.plot(sizes, times_pivot, label="z pivotingiem")
+ plt.xlabel("Rozmiar macierzy (n)")
+ plt.ylabel("Czas [s]")
+ plt.title("LU faktoryzacja – porównanie")
+ plt.legend()
+ plt.grid()
+ plt.savefig(path)
+
+ print(f"[SAVE] {path}")
+
+
+def print_example_results():
+ M = [
+ [2.0, 1.0, 1.0],
+ [4.0, -6.0, 0.0],
+ [-2.0, 7.0, 2.0],
+ ]
+
+ print("\n====================")
+ print("MACIERZ WEJŚCIOWA")
+ print("====================")
+ for row in M:
+ print(row)
+
+ print("\n====================")
+ print("GAUSS BEZ PIVOTINGU")
+ print("====================")
+ result = MatrixProcessor.gaussian_elimination(M, pivoting=False)
+ for row in result:
+ print(row)
+
+ print("\n====================")
+ print("GAUSS Z PIVOTINGIEM (bez normalizacji przekątnej)")
+ print("====================")
+ result = MatrixProcessor.gaussian_elimination(
+ M,
+ pivoting=True,
+ normalize_diagonal=False,
+ )
+ for row in result:
+ print(row)
+
+ print("\n====================")
+ print("LU BEZ PIVOTINGU")
+ print("====================")
+ L, U = LUDecomposition.decompose(M, pivoting=False)
+ print("L:")
+ for row in L:
+ print(row)
+ print("U:")
+ for row in U:
+ print(row)
+
+ print("\n====================")
+ print("LU Z PIVOTINGIEM")
+ print("====================")
+ P, L, U = LUDecomposition.decompose(M, pivoting=True)
+ print("P:")
+ for row in P:
+ print(row)
+ print("L:")
+ for row in L:
+ print(row)
+ print("U:")
+ for row in U:
+ print(row)
+
+
+def benchmark_gaussian_elimination_and_lu_decomposition(out_dir: str, n_max: int):
+ os.makedirs(out_dir, exist_ok=True)
+
+ benchmark_gaussian_elimination(out_dir, n_max)
+ benchmark_lu_decomposition(out_dir, n_max)
+ print_example_results()
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument(
+ "--output",
+ type=str,
+ default="results",
+ help="folder na wykresy",
+ )
+
+ parser.add_argument(
+ "--n",
+ type=int,
+ default=150,
+ help="maksymalny rozmiar macierzy",
+ )
+
+ args = parser.parse_args()
+
+ benchmark_gaussian_elimination_and_lu_decomposition(
+ out_dir=args.output,
+ n_max=args.n,
+ )
diff --git a/matrix_calculus/linalg/__init__.py b/matrix_calculus/linalg/__init__.py
new file mode 100644
index 0000000..e24b365
--- /dev/null
+++ b/matrix_calculus/linalg/__init__.py
@@ -0,0 +1,6 @@
+from __future__ import annotations
+
+from .matrix_properties import det
+from .matrix_properties import rank
+
+__all__ = ["det", "rank"]
diff --git a/matrix_calculus/linalg/matrix_properties.py b/matrix_calculus/linalg/matrix_properties.py
new file mode 100644
index 0000000..3056a74
--- /dev/null
+++ b/matrix_calculus/linalg/matrix_properties.py
@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+from ..matrix_processor import MatrixProcessor
+
+
+def det(matrix: list[list[float]]) -> float:
+ """Calculate the determinant of a square matrix."""
+ processed_matrix, stats = MatrixProcessor.gaussian_elimination(
+ matrix,
+ stats=True,
+ normalize_diagonal=False,
+ )
+
+ if not stats["reversible"]:
+ return 0.0
+
+ determinant = (-1) ** stats["rows_swaps"]
+ for i in range(len(processed_matrix)):
+ determinant *= processed_matrix[i][i]
+
+ return determinant
+
+
+def rank(matrix: list[list[float]]) -> float:
+ """Calculate the rank of a matrix."""
+ _, stats = MatrixProcessor.gaussian_elimination(
+ matrix,
+ stats=True,
+ normalize_diagonal=False,
+ )
+
+ return stats["rank"]
diff --git a/matrix_calculus/matrix_processor.py b/matrix_calculus/matrix_processor.py
index b3f9652..17ddabe 100644
--- a/matrix_calculus/matrix_processor.py
+++ b/matrix_calculus/matrix_processor.py
@@ -1,12 +1,61 @@
from __future__ import annotations
+from typing import Any
+from typing import Literal
+from typing import overload
+
class MatrixProcessor:
+ @overload
+ @staticmethod
+ def gaussian_elimination(
+ matrix: list[list[float]],
+ pivoting: bool = ...,
+ stats: Literal[True] = ...,
+ normalize_diagonal: bool = ...,
+ ) -> tuple[list[list[float]], dict[str, Any]]:
+ pass
+
+ @overload
@staticmethod
- def gaussian_elimination(matrix: list[list[float]]) -> list[list[float]]:
+ def gaussian_elimination(
+ matrix: list[list[float]],
+ pivoting: bool = ...,
+ stats: Literal[False] = ...,
+ normalize_diagonal: bool = ...,
+ ) -> list[list[float]]:
+ pass
+
+ @staticmethod
+ def gaussian_elimination(
+ matrix: list[list[float]],
+ pivoting: bool = True,
+ stats: bool = False,
+ normalize_diagonal: bool = True,
+ ) -> list[list[float]] | tuple[list[list[float]], dict]:
+ if pivoting:
+ result = MatrixProcessor.gaussian_elimination_with_pivoting(
+ matrix,
+ normalize_diagonal,
+ )
+ else:
+ result = MatrixProcessor.gaussian_elimination_without_pivoting(
+ matrix,
+ normalize_diagonal,
+ )
+
+ return result[0] if not stats else result
+
+ @staticmethod
+ def gaussian_elimination_without_pivoting(
+ matrix: list[list[float]],
+ normalize_diagonal: bool = True,
+ ) -> tuple[list[list[float]], dict]:
M = [row[:] for row in matrix]
n = len(M)
eps = 1e-12
+ rows_swaps = 0
+ rank = n
def swap_rows(row1, row2):
for col in range(n):
@@ -22,25 +71,37 @@ def swap_rows(row1, row2):
diagonal_element_value = M[row][diagonal_element]
swap_rows(diagonal_element, row)
+ rows_swaps += 1
if abs(diagonal_element_value) < eps:
- raise ValueError("Matrix is singular")
+ rank -= 1
+ continue
- for col in range(diagonal_element, n):
- M[diagonal_element][col] /= diagonal_element_value
+ if normalize_diagonal:
+ for col in range(diagonal_element, n):
+ M[diagonal_element][col] /= diagonal_element_value
for row in range(diagonal_element + 1, n):
- factor = M[row][diagonal_element]
+ factor = (
+ M[row][diagonal_element]
+ if normalize_diagonal
+ else M[row][diagonal_element] / diagonal_element_value
+ )
for col in range(diagonal_element, n):
M[row][col] -= factor * M[diagonal_element][col]
- return M
+ return M, {"rows_swaps": rows_swaps, "rank": rank, "reversible": rank == n}
@staticmethod
- def gaussian_elimination_pivoting(matrix: list[list[float]]) -> list[list[float]]:
+ def gaussian_elimination_with_pivoting(
+ matrix: list[list[float]],
+ normalize_diagonal: bool = True,
+ ) -> tuple[list[list[float]], dict]:
M = [row[:] for row in matrix]
n = len(M)
eps = 1e-12
+ rows_swaps = 0
+ rank = n
def swap_rows(row1, row2):
for col in range(n):
@@ -57,14 +118,24 @@ def swap_rows(row1, row2):
if highest_pivot_row != diagonal_element:
swap_rows(diagonal_element, highest_pivot_row)
+ rows_swaps += 1
pivot_value = M[diagonal_element][diagonal_element]
if abs(pivot_value) < eps:
- raise ValueError("Matrix is singular")
+ rank -= 1
+ continue
+
+ if normalize_diagonal:
+ for col in range(diagonal_element, n):
+ M[diagonal_element][col] /= pivot_value
for row in range(diagonal_element + 1, n):
- factor = M[row][diagonal_element] / pivot_value
+ factor = (
+ M[row][diagonal_element]
+ if normalize_diagonal
+ else M[row][diagonal_element] / pivot_value
+ )
for col in range(diagonal_element, n):
M[row][col] -= factor * M[diagonal_element][col]
- return M
+ return M, {"rows_swaps": rows_swaps, "rank": rank, "reversible": rank == n}
diff --git a/reports/imgs/gauss_benchmark.png b/reports/imgs/gauss_benchmark.png
new file mode 100644
index 0000000..d5f0e9c
Binary files /dev/null and b/reports/imgs/gauss_benchmark.png differ
diff --git a/reports/imgs/lu_benchmark.png b/reports/imgs/lu_benchmark.png
new file mode 100644
index 0000000..6e940da
Binary files /dev/null and b/reports/imgs/lu_benchmark.png differ
diff --git a/reports/matrix_gaussian_elimination_and_lu_decomposition_report.md b/reports/matrix_gaussian_elimination_and_lu_decomposition_report.md
new file mode 100644
index 0000000..15cfe0b
--- /dev/null
+++ b/reports/matrix_gaussian_elimination_and_lu_decomposition_report.md
@@ -0,0 +1,220 @@
+# Eliminacja Gaussa i LU faktoryzacja
+
+*Autorzy: Maja Byrecka, Michał Kowalczyk*
+
+## Eliminacja Gaussa bez pivotingu
+
+### Pseudokod
+
+```pseudo
+Wejście: macierz A ∈ ℝ^{n×n}
+
+Dla k = 0 do n-1:
+
+ pivot = A[k][k]
+
+ Jeśli |pivot| < ε:
+ Spróbuj znaleźć wiersz i > k taki, że |A[i][k]| > ε
+ Jeśli taki wiersz istnieje:
+ zamień wiersz k z i
+ pivot = A[k][k]
+ W przeciwnym razie:
+ zgłoś błąd (macierz osobliwa)
+
+ # normalizacja wiersza pivotowego
+ Dla j = k do n-1:
+ A[k][j] = A[k][j] / pivot
+
+ # eliminacja poniżej pivota
+ Dla i = k+1 do n-1:
+ factor = A[i][k]
+ Dla j = k do n-1:
+ A[i][j] = A[i][j] - factor * A[k][j]
+
+Wyjście: macierz w postaci trójkątnej górnej
+```
+
+### Złożoność
+
+- Czasowa: \( O(n^3) \)
+- Pamięciowa: \( O(n^2) \)
+ (tworzona jest kopia macierzy; możliwy wariant in-place)
+
+## Eliminacja Gaussa z pivotingiem
+
+### Pseudokod
+
+```pseudo
+Wejście: macierz A ∈ ℝ^{n×n}
+
+Dla k = 0 do n-1:
+
+ # wybór najlepszego pivota (partial pivoting)
+ max_row = k
+ max_value = |A[k][k]|
+
+ Dla i = k+1 do n-1:
+ Jeśli |A[i][k]| > max_value:
+ max_value = |A[i][k]|
+ max_row = i
+
+ Jeśli max_row ≠ k:
+ zamień wiersz k z max_row
+
+ pivot = A[k][k]
+
+ Jeśli |pivot| < ε:
+ zgłoś błąd (macierz osobliwa)
+
+ # eliminacja poniżej pivota
+ Dla i = k+1 do n-1:
+ factor = A[i][k] / pivot
+ Dla j = k do n-1:
+ A[i][j] = A[i][j] - factor * A[k][j]
+
+Wyjście: macierz w postaci trójkątnej górnej
+```
+
+### Przykładowe wyniki
+
+Dla macierzy wejściowej:
+`M = [[2.0, 1.0, 1.0], [4.0, -6.0, 0.0], [-2.0, 7.0, 2.0]]`
+
+- Eliminacja Gaussa bez pivotingu:
+`[[1.0, 0.5, 0.5], [0.0, 1.0, 0.25], [0.0, 0.0, 1.0]]`
+
+- Eliminacja Gasussa z pivotingiem (bez normalizacji przekątnej):
+`[[4.0, -6.0, 0.0], [0.0, 4.0, 1.0], [0.0, 0.0, 1.0]]`
+
+### Złożoność
+
+- Czasowa: \( O(n^3) \)
+- Pamięciowa: \( O(n^2) \)
+
+---
+
+## LU faktoryzacja bez pivotingu
+
+### Pseudokod
+
+```pseudo
+Wejście: macierz A ∈ ℝ^{n×n}
+
+Inicjalizuj:
+ L = macierz zerowa n×n
+ U = macierz zerowa n×n
+
+Dla i = 0 do n-1:
+
+ # wyznaczanie wiersza U
+ Dla k = i do n-1:
+ sum = 0
+ Dla j = 0 do i-1:
+ sum = sum + L[i][j] * U[j][k]
+ U[i][k] = A[i][k] - sum
+
+ L[i][i] = 1
+
+ # wyznaczanie kolumny L
+ Dla k = i+1 do n-1:
+ sum = 0
+ Dla j = 0 do i-1:
+ sum = sum + L[k][j] * U[j][i]
+
+ Jeśli U[i][i] == 0:
+ zgłoś błąd (dzielenie przez zero)
+
+ L[k][i] = (A[k][i] - sum) / U[i][i]
+
+Wyjście: macierze L (dolnotrójkątna) i U (górnotrójkątna)
+```
+
+### Złożoność
+
+- Czasowa: \( O(n^3) \)
+- Pamięciowa: \( O(n^2) \)
+
+---
+
+## LU faktoryzacja z pivotingiem
+
+### Pseudokod
+
+```pseudo
+Wejście: macierz A ∈ ℝ^{n×n}
+
+Inicjalizuj:
+ U = kopia A
+ L = macierz zerowa n×n
+ P = macierz jednostkowa n×n
+
+Dla k = 0 do n-1:
+
+ # wybór pivota (partial pivoting)
+ pivot = indeks i ≥ k maksymalizujący |U[i][k]|
+
+ Jeśli |U[pivot][k]| < ε:
+ zgłoś błąd (macierz osobliwa)
+
+ # zamiana wierszy
+ zamień wiersze k i pivot w U
+ zamień wiersze k i pivot w P
+
+ Jeśli k > 0:
+ zamień odpowiednie elementy w L (kolumny < k)
+
+ # eliminacja
+ Dla j = k+1 do n-1:
+ L[j][k] = U[j][k] / U[k][k]
+ Dla col = 0 do n-1:
+ U[j][col] = U[j][col] - L[j][k] * U[k][col]
+
+Dla i = 0 do n-1:
+ L[i][i] = 1
+
+Wyjście: macierze P, L, U takie, że PA = LU
+```
+
+### Przykładowe wyniki
+
+Dla macierzy wejściowej:
+`M = [[2.0, 1.0, 1.0], [4.0, -6.0, 0.0], [-2.0, 7.0, 2.0]]`
+
+- LU faktoryzacja bez pivotingu:
+`L = [[1.0, 0.0, 0.0], [2.0, 1.0, 0.0], [-1.0, -1.0, 1.0]]`
+`U = [[2.0, 1.0, 1.0], [0.0, -8.0, -2.0], [0.0, 0.0, 1.0]]`
+
+- LU faktoryzacja z pivotingiem:
+`P = [[0. 1. 0.], [1. 0. 0.], [0. 0. 1.]]`
+`L = [[1. 0. 0.], [0.5 1. 0.], [-0.5 1. 1.]]`
+`U = [[ 4. -6. 0.], [0. 4. 1.], [0. 0. 1.]]`
+
+### Złożoność
+
+- Czasowa: \( O(n^3) \)
+- Pamięciowa: \( O(n^2) \)
+
+## Porównanie czasów wykonywania algorytmów
+
+### Eliminacja Gaussa
+
+Obie wersje algorytmu mają taką samą złożoność obliczeniową, więc zgodnie z oczekiwaniami wykresy czasów ich wykonywania się pokrywają.
+
+
+
+### LU faktoryzacja
+
+Tak samo jak w przypadku eliminacji Gaussa, obie wersje algorytmu faktoryzacji LU mają taką samą złożoność obliczeniową. Powodem rozbieżności jest różnica implementacji między nimi - wersja z pivotingiem wykonuje się znacznie szybciej ze względu na użycie biblioteki numerycznej przyspieszającej operacje na tablicach.
+
+
+
+
+
+---
+
+Wykonanie:
+
+* Język: Python3
+* Podział zadań:
+ * Faktoryzacja LU z pivotingiem i bez pivotingu - Maja Byrecka
+ * Eliminacja Gaussa z pivotingiem i bez pivotingu - Michał Kowalczyk
diff --git a/reports/resources/gaussian_elimination_and_lu_decomposition/code.txt b/reports/resources/gaussian_elimination_and_lu_decomposition/code.txt
new file mode 100644
index 0000000..4b327a7
--- /dev/null
+++ b/reports/resources/gaussian_elimination_and_lu_decomposition/code.txt
@@ -0,0 +1,380 @@
+
+========== lu_decomposition_test.py ==========
+from __future__ import annotations
+
+import numpy as np
+import pytest # noqa: F401
+
+from matrix_calculus.lu_decomposition import LUDecomposition
+from matrix_calculus.matrix_multiplier import MatrixMultiplier
+
+
+def test_lu_no_pivot():
+ matrix = [[1, 2, 3], [5, 6, 7], [9, 10, 11]]
+ L, U = LUDecomposition.decompose(matrix=matrix, pivoting=False)
+ assert L == [[1, 0, 0], [5, 1, 0], [9, 2, 1]]
+ assert U == [[1, 2, 3], [0, -4, -8], [0, 0, 0]]
+
+
+def test_lu_with_pivot_singular():
+ # Test singular matrix (determinant equal to 0)
+ with pytest.raises(ZeroDivisionError):
+ matrix = [[1, 2, 3], [5, 6, 7], [9, 10, 11]]
+ LUDecomposition.decompose(matrix=matrix, pivoting=True)
+
+
+def test_lu_with_pivot_correct():
+ # Test correct decomposition
+ matrix = [[1, 2, 3], [4, 5, 6], [8, 10, 10]]
+ P, L, U = LUDecomposition.decompose(matrix=matrix, pivoting=True)
+ L == [[1, 0, 0], [4, 1, 0], [8, 2, 1]]
+ assert np.allclose(
+ MatrixMultiplier.traditional_multiplication(P, matrix),
+ MatrixMultiplier.traditional_multiplication(L, U),
+ )
+
+
+def test_lu_example():
+ date = 8 + 24
+ np.random.seed(date)
+ matrix = np.random.rand(date, date) * 10 // 1
+ # Make sure that generated matrix is not singular
+ matrix += np.eye(date) * 100
+
+ # LU without pivoting
+ L, U = LUDecomposition.decompose(matrix, pivoting=False)
+ assert np.allclose(
+ MatrixMultiplier.traditional_multiplication(L, U),
+ matrix,
+ )
+
+ # LU with pivoting
+ P, L, U = LUDecomposition.decompose(matrix, pivoting=True)
+ assert np.allclose(
+ MatrixMultiplier.traditional_multiplication(L, U),
+ MatrixMultiplier.traditional_multiplication(P, matrix),
+ )
+
+
+========== matrix_multiplier.py ==========
+from __future__ import annotations
+
+from matrix_calculus.utils import _next_power_of_2
+from matrix_calculus.utils import _pad_matrix
+from matrix_calculus.utils import _strassen
+from matrix_calculus.utils import _unpad_matrix
+
+
+class MatrixMultiplier:
+
+ @staticmethod
+ def traditional_multiplication(
+ matrix_1: list[list[float]],
+ matrix_2: list[list[float]],
+ ) -> list[list[float]]:
+
+ if len(matrix_1[0]) != len(matrix_2):
+ raise ValueError(
+ "Invalid matrices sizes. Given matrices cannot be multiplied.",
+ )
+
+ n_rows = len(matrix_1)
+ n_cols = len(matrix_2[0])
+
+ res = [[0.0 for _ in range(n_cols)] for _ in range(n_rows)]
+
+ for row in range(n_rows):
+ for col in range(n_cols):
+ for k in range(len(matrix_1[0])):
+ res[row][col] += matrix_1[row][k] * matrix_2[k][col]
+
+ return res
+
+ @staticmethod
+ def strassen_matrix_multiplication(
+ matrix_1: list[list[float]],
+ matrix_2: list[list[float]],
+ ) -> list[list[float]]:
+
+ rows_1 = len(matrix_1)
+ cols_1 = len(matrix_1[0])
+ rows_2 = len(matrix_2)
+ cols_2 = len(matrix_2[0])
+
+ if not (rows_1 == cols_1 and rows_2 == cols_2 and rows_1 == rows_2):
+ raise ValueError(
+ "Both matrices must be squared matrices of the same size",
+ )
+
+ n = len(matrix_1)
+
+ if n == 1:
+ return [[matrix_1[0][0] * matrix_2[0][0]]]
+
+ m = _next_power_of_2(n)
+
+ if m == n:
+ return _strassen(matrix_1, matrix_2, n)
+
+ A_pad = _pad_matrix(matrix_1, m)
+ B_pad = _pad_matrix(matrix_2, m)
+
+ C_pad = _strassen(A_pad, B_pad, m)
+
+ return _unpad_matrix(C_pad, n)
+
+
+========== utils.py ==========
+from __future__ import annotations
+
+
+def _zeroes_matrix(n):
+ return [[0 for _ in range(n)] for _ in range(n)]
+
+
+# sub_matrix is a tuple of the form (matrix, row_start, col_start)
+
+
+def _add_sub_matrices(
+ sub_matrix_1,
+ sub_matrix_2,
+ n,
+ result,
+ result_matrix_offset=(0, 0),
+):
+ m1, r1, c1 = sub_matrix_1
+ m2, r2, c2 = sub_matrix_2
+ result_offset_row, result_offset_col = result_matrix_offset
+ for i in range(n):
+ for j in range(n):
+ result[result_offset_row + i][result_offset_col + j] = (
+ m1[r1 + i][c1 + j] + m2[r2 + i][c2 + j]
+ )
+ return result
+
+
+def _subtract_sub_matrices(
+ sub_matrix_1,
+ sub_matrix_2,
+ n,
+ result,
+ result_matrix_offset=(0, 0),
+):
+ m1, r1, c1 = sub_matrix_1
+ m2, r2, c2 = sub_matrix_2
+ result_offset_row, result_offset_col = result_matrix_offset
+ for i in range(n):
+ for j in range(n):
+ result[result_offset_row + i][result_offset_col + j] = (
+ m1[r1 + i][c1 + j] - m2[r2 + i][c2 + j]
+ )
+ return result
+
+
+def _set_sub_matrix(sub_matrix, other_matrix, n):
+ m, r, c = sub_matrix
+ for i in range(n):
+ for j in range(n):
+ m[i + r][j + c] = other_matrix[i][j]
+ return m
+
+
+def _sub_matrix_to_matrix(sub_matrix, n, result):
+ m, r, c = sub_matrix
+ for i in range(n):
+ for j in range(n):
+ result[i][j] = m[r + i][c + j]
+ return result
+
+
+def _add_matrices(*matrices, result=None):
+ assert result is not None
+ for i in range(len(result)):
+ for j in range(len(result[0])):
+ result[i][j] = matrices[0][i][j]
+ for k in range(1, len(matrices)):
+ result[i][j] += matrices[k][i][j]
+ return result
+
+
+def _subtract_matrix_from(target_matrix, matrix_to_subtract):
+ for i in range(len(target_matrix)):
+ for j in range(len(target_matrix[0])):
+ target_matrix[i][j] -= matrix_to_subtract[i][j]
+ return target_matrix
+
+
+def _strassen(matrix_1, matrix_2, n):
+ if n == 2:
+ c11 = matrix_1[0][0] * matrix_2[0][0] + matrix_1[0][1] * matrix_2[1][0]
+ c12 = matrix_1[0][0] * matrix_2[0][1] + matrix_1[0][1] * matrix_2[1][1]
+ c21 = matrix_1[1][0] * matrix_2[0][0] + matrix_1[1][1] * matrix_2[1][0]
+ c22 = matrix_1[1][0] * matrix_2[0][1] + matrix_1[1][1] * matrix_2[1][1]
+ return [[c11, c12], [c21, c22]]
+
+ else:
+ sub_matrix_dim = n >> 1
+
+ buffer_1 = _zeroes_matrix(sub_matrix_dim)
+ buffer_2 = _zeroes_matrix(sub_matrix_dim)
+
+ a11 = (matrix_1, 0, 0)
+ a12 = (matrix_1, 0, sub_matrix_dim)
+ a21 = (matrix_1, sub_matrix_dim, 0)
+ a22 = (matrix_1, sub_matrix_dim, sub_matrix_dim)
+
+ b11 = (matrix_2, 0, 0)
+ b12 = (matrix_2, 0, sub_matrix_dim)
+ b21 = (matrix_2, sub_matrix_dim, 0)
+ b22 = (matrix_2, sub_matrix_dim, sub_matrix_dim)
+
+ m1 = _strassen(
+ _add_sub_matrices(a11, a22, sub_matrix_dim, buffer_1),
+ _add_sub_matrices(b11, b22, sub_matrix_dim, buffer_2),
+ sub_matrix_dim,
+ )
+ m2 = _strassen(
+ _add_sub_matrices(a21, a22, sub_matrix_dim, buffer_1),
+ _sub_matrix_to_matrix(b11, sub_matrix_dim, buffer_2),
+ sub_matrix_dim,
+ )
+ m3 = _strassen(
+ _sub_matrix_to_matrix(a11, sub_matrix_dim, buffer_1),
+ _subtract_sub_matrices(b12, b22, sub_matrix_dim, buffer_2),
+ sub_matrix_dim,
+ )
+ m4 = _strassen(
+ _sub_matrix_to_matrix(a22, sub_matrix_dim, buffer_1),
+ _subtract_sub_matrices(b21, b11, sub_matrix_dim, buffer_2),
+ sub_matrix_dim,
+ )
+ m5 = _strassen(
+ _add_sub_matrices(a11, a12, sub_matrix_dim, buffer_1),
+ _sub_matrix_to_matrix(b22, sub_matrix_dim, buffer_2),
+ sub_matrix_dim,
+ )
+ m6 = _strassen(
+ _subtract_sub_matrices(a21, a11, sub_matrix_dim, buffer_1),
+ _add_sub_matrices(b11, b12, sub_matrix_dim, buffer_2),
+ sub_matrix_dim,
+ )
+ m7 = _strassen(
+ _subtract_sub_matrices(a12, a22, sub_matrix_dim, buffer_1),
+ _add_sub_matrices(b21, b22, sub_matrix_dim, buffer_2),
+ sub_matrix_dim,
+ )
+
+ c = _zeroes_matrix(n)
+
+ c11 = _add_matrices(m1, m4, m7, result=buffer_1)
+ _subtract_matrix_from(c11, m5)
+ _set_sub_matrix((c, 0, 0), c11, sub_matrix_dim)
+
+ c12 = _add_matrices(m3, m5, result=buffer_1)
+ _set_sub_matrix((c, 0, sub_matrix_dim), c12, sub_matrix_dim)
+
+ c21 = _add_matrices(m2, m4, result=buffer_1)
+ _set_sub_matrix((c, sub_matrix_dim, 0), c21, sub_matrix_dim)
+
+ c22 = _add_matrices(m1, m3, m6, result=buffer_1)
+ _subtract_matrix_from(c22, m2)
+ _set_sub_matrix(
+ (c, sub_matrix_dim, sub_matrix_dim),
+ c22,
+ sub_matrix_dim,
+ )
+
+ return c
+
+
+def _next_power_of_2(n):
+ return 1 if n == 0 else 2 ** ((n - 1).bit_length())
+
+
+def _pad_matrix(matrix, new_size):
+ old_size = len(matrix)
+ padded = [[0] * new_size for _ in range(new_size)]
+
+ for i in range(old_size):
+ for j in range(old_size):
+ padded[i][j] = matrix[i][j]
+
+ return padded
+
+
+def _unpad_matrix(matrix, size):
+ return [row[:size] for row in matrix[:size]]
+
+
+========== lu_decomposition.py ==========
+from __future__ import annotations
+
+import numpy as np
+
+
+class LUDecomposition:
+ @staticmethod
+ def decompose(
+ matrix: list[list[float]],
+ pivoting=False,
+ ) -> tuple:
+ if pivoting is True:
+ return LUDecomposition.decompose_with_pivot(matrix=matrix)
+ else:
+ return LUDecomposition.decompose_no_pivot(matrix=matrix)
+
+ @staticmethod
+ def decompose_no_pivot(
+ matrix: list[list[float]],
+ ) -> tuple[list[list[float]], list[list[float]]]:
+ n = len(matrix)
+ L = [[0.0] * n for _ in range(n)]
+ U = [[0.0] * n for _ in range(n)]
+
+ for i in range(n):
+ for k in range(i, n):
+ sum_u = sum(L[i][j] * U[j][k] for j in range(i))
+ U[i][k] = matrix[i][k] - sum_u
+ L[i][i] = 1.0
+ for k in range(i + 1, n):
+ sum_l = sum(L[k][j] * U[j][i] for j in range(i))
+ if U[i][i] == 0:
+ raise ZeroDivisionError(
+ "U diagonal element equals zero. Cannot decompose without pivoting.",
+ )
+ L[k][i] = (matrix[k][i] - sum_l) / U[i][i]
+
+ return L, U
+
+ @staticmethod
+ def decompose_with_pivot(
+ matrix: list[list[float]],
+ ) -> tuple[list[list[float]], list[list[float]], list[list[float]]]:
+ A = np.array(matrix).copy().astype(float)
+ n = A.shape[0]
+
+ L = np.zeros((n, n))
+ U = A.copy()
+ P = np.eye(n)
+
+ for k in range(n):
+ pivot = np.argmax(np.abs(U[k:n, k])) + k
+ if U[pivot, k] < 1e-5:
+ raise ZeroDivisionError(
+ "Matrix is singular. Cannot use LU decomposition with pivoting.",
+ )
+
+ # Switch rows
+ U[[k, pivot], :] = U[[pivot, k], :]
+ P[[k, pivot], :] = P[[pivot, k], :]
+ if k > 0:
+ L[[k, pivot], :k] = L[[pivot, k], :k]
+
+ for j in range(k + 1, n):
+ L[j, k] = U[j, k] / U[k, k]
+ U[j, :] = U[j, :] - L[j, k] * U[k, :]
+
+ for i in range(n):
+ L[i, i] = 1.0
+
+ return P, L, U
diff --git a/reports/resources/gaussian_elimination_and_lu_decomposition/report.pdf b/reports/resources/gaussian_elimination_and_lu_decomposition/report.pdf
new file mode 100644
index 0000000..273d571
Binary files /dev/null and b/reports/resources/gaussian_elimination_and_lu_decomposition/report.pdf differ
diff --git a/tests/gaussian_elimination_test.py b/tests/gaussian_elimination_test.py
index c78e090..99dce8e 100644
--- a/tests/gaussian_elimination_test.py
+++ b/tests/gaussian_elimination_test.py
@@ -6,44 +6,68 @@
def test_gaussian_elimination():
+ print("\nTesting Gaussian elimination without pivoting...")
+
# Test 1x1 matrix
matrix = [[2]]
- res = MatrixProcessor.gaussian_elimination(matrix)
+ res = MatrixProcessor.gaussian_elimination(matrix, pivoting=False)
assert res == [[1.0]]
# Test 2x2 matrix
matrix = [[2, 4], [1, 3]]
- res = MatrixProcessor.gaussian_elimination(matrix)
+ res = MatrixProcessor.gaussian_elimination(matrix, pivoting=False)
assert res == [[1.0, 2.0], [0.0, 1.0]]
# Test 3x3 singular matrix
- with pytest.raises(ValueError):
- matrix = [[2, 4, 6], [1, 3, 5], [0, 2, 4]]
- MatrixProcessor.gaussian_elimination(matrix)
+ matrix = [[2, 4, 6], [1, 3, 5], [0, 2, 4]]
+ res, stats = MatrixProcessor.gaussian_elimination(
+ matrix,
+ pivoting=False,
+ stats=True,
+ )
+ print(res)
+ print(stats)
# Test 3x3 matrix
matrix = [[2, 4, 6], [1, 2, 5], [0, 2, 4]]
- res = MatrixProcessor.gaussian_elimination(matrix)
+ res = MatrixProcessor.gaussian_elimination(matrix, pivoting=False)
assert res == [[1.0, 2.0, 3.0], [0.0, 1.0, 2.0], [0.0, 0.0, 1.0]]
def test_gaussian_elimination_pivoting():
+ print("\nTesting Gaussian elimination with pivoting...")
+
# Test 1x1 matrix
matrix = [[2]]
- res = MatrixProcessor.gaussian_elimination_pivoting(matrix)
+ res = MatrixProcessor.gaussian_elimination(matrix, normalize_diagonal=False)
assert res == [[2.0]]
# Test 2x2 matrix
matrix = [[2, 4], [1, 3]]
- res = MatrixProcessor.gaussian_elimination_pivoting(matrix)
+ res = MatrixProcessor.gaussian_elimination(matrix, normalize_diagonal=False)
assert res == [[2.0, 4.0], [0.0, 1.0]]
+ # Test 2x2 singular matrix
+ matrix = [[2, 4], [4, 8]]
+ res, stats = MatrixProcessor.gaussian_elimination(
+ matrix,
+ normalize_diagonal=False,
+ stats=True,
+ )
+ print(res)
+ print(stats)
+
# Test 3x3 singular matrix
- with pytest.raises(ValueError):
- matrix = [[2, 4, 6], [1, 3, 5], [0, 2, 4]]
- MatrixProcessor.gaussian_elimination_pivoting(matrix)
+ matrix = [[2, 4, 6], [1, 3, 5], [0, 2, 4]]
+ res, stats = MatrixProcessor.gaussian_elimination(
+ matrix,
+ stats=True,
+ normalize_diagonal=False,
+ )
+ print(res)
+ print(stats)
# Test 3x3 matrix
matrix = [[2, 4, 6], [1, 2, 5], [0, 2, 4]]
- res = MatrixProcessor.gaussian_elimination_pivoting(matrix)
+ res = MatrixProcessor.gaussian_elimination(matrix, normalize_diagonal=False)
assert res == [[2, 4, 6], [0, 2, 4], [0, 0, 2]]
diff --git a/tests/linalg_matrix_properties_test.py b/tests/linalg_matrix_properties_test.py
new file mode 100644
index 0000000..3defc6c
--- /dev/null
+++ b/tests/linalg_matrix_properties_test.py
@@ -0,0 +1,54 @@
+from __future__ import annotations
+
+import numpy as np
+import pytest # noqa: F401
+
+import matrix_calculus.linalg as linalg
+
+
+def test_determinant():
+ # Test 1x1 matrix
+ matrix = [[5]]
+ assert linalg.det(matrix) == 5.0
+
+ # Test 2x2 matrix
+ matrix = [[1, 2], [3, 4]]
+ assert linalg.det(matrix) == -2.0
+
+ # Test 3x3 matrix
+ matrix = [[6, 1, 1], [4, -2, 5], [2, 8, 7]]
+ assert linalg.det(matrix) == np.linalg.det(matrix)
+
+ # Test 2x2 singular matrix
+ matrix = [[2, 4], [1, 2]]
+ assert linalg.det(matrix) == 0.0
+
+ # Test 3x3 singular matrix
+ matrix = [[2, 4, 6], [1, 2, 3], [1, 7, 5]]
+ assert linalg.det(matrix) == 0.0
+
+
+def test_rank():
+ # Test 1x1 matrix
+ matrix = [[5]]
+ assert linalg.rank(matrix) == 1
+
+ # Test 2x2 full rank matrix
+ matrix = [[1, 2], [3, 4]]
+ assert linalg.rank(matrix) == 2
+
+ # Test 3x3 full rank matrix
+ matrix = [[6, 1, 1], [4, -2, 5], [2, 8, 7]]
+ assert linalg.rank(matrix) == np.linalg.matrix_rank(matrix)
+
+ # Test 2x2 singular matrix
+ matrix = [[2, 4], [1, 2]]
+ assert linalg.rank(matrix) == 1
+
+ # Test 3x3 singular matrix
+ matrix = [[2, 4, 6], [1, 2, 3], [1, 7, 5]]
+ assert linalg.rank(matrix) == 2
+
+ # Test 3x3 singular matrix
+ matrix = [[2, 4, 6], [1, 2, 3], [4, 8, 12]]
+ assert linalg.rank(matrix) == 1