diff --git a/CHANGELOG.md b/CHANGELOG.md index 532a0c4..09de071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [1.3.0] - 2026-03-20 + +### Added + +- Matrix multiplication report + +### Fixed + +- `md` report generation - images and code blocks + ## [1.2.0] - 2026-03-19 ### Added diff --git a/generate_report.py b/generate_report.py index 4f4f74d..c48e260 100755 --- a/generate_report.py +++ b/generate_report.py @@ -15,8 +15,11 @@ def md_to_pdf(md_path: Path, pdf_path: Path): with open(md_path, encoding="utf-8") as f: md_text = f.read() - html = markdown.markdown(md_text) - HTML(string=html).write_pdf(str(pdf_path)) + html = markdown.markdown( + md_text, + extensions=["fenced_code", "codehilite"], + ) + HTML(string=html, base_url=md_path.parent).write_pdf(str(pdf_path)) def resolve_module_path(module_name): diff --git a/pyproject.toml b/pyproject.toml index c2847a0..a302f88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "matrix_calculus" -version = "1.2.0" +version = "1.3.0" description = "Matrix calculus toolkit" requires-python = ">=3.10" dependencies = ["numpy", "seaborn", "matplotlib", "weasyprint", "markdown"] diff --git a/reports/imgs/execution_time.png b/reports/imgs/execution_time.png new file mode 100644 index 0000000..1173168 Binary files /dev/null and b/reports/imgs/execution_time.png differ diff --git a/reports/imgs/image_time_logx.png b/reports/imgs/image_time_logx.png new file mode 100644 index 0000000..929d96c Binary files /dev/null and b/reports/imgs/image_time_logx.png differ diff --git a/reports/imgs/time_benchmark.jpeg b/reports/imgs/time_benchmark.jpeg new file mode 100644 index 0000000..7126cf0 Binary files /dev/null and b/reports/imgs/time_benchmark.jpeg differ diff --git a/reports/matrix_multiplication_report.md b/reports/matrix_multiplication_report.md new file mode 100644 index 0000000..7747e5f --- /dev/null +++ b/reports/matrix_multiplication_report.md @@ -0,0 +1,123 @@ +# Mnożenie Macierzy + +*Autorzy: Maja Byrecka, Michał Kowalczyk* + +## Tradycyjne mnożenie macierzy + +### Pseudokod + +Mnożenie macierzy kwadratowych o rozmiarach `SIZE x SIZE` + +``` +for (i = 0; i < SIZE; i++) { + for (j = 0; j < SIZE; j++) { + for (k = 0; k < SIZE; k++) { + s = s + m_1[i][k]*m_2[k][j]; + } + res[i][j] = s; + s = 0; + } + } +return res; +``` + +### Złożoność + +Tradycyjne mnożenie macierzy posiada ogólną złożoność n^3 + +Ilość operacji podczas mnożenia macierzy k x k: + +* Mnożenia: k^3 +* Dodawania: k^2 * (k-1) +* Dzielenia: 0 +* Odejmowania: 0 + +Algorytm nie wymaga alokacji dodatkowej pamięci (poza pamięcią na macierz wynikową), w trakcie wykonywania operacji. + +## Algorytm Strassena + +### Pseudokod + +``` +Strassen – funkcja główna z dopełnieniem do potęgi 2 +Input: A ∈ Rn×n, B ∈ Rn×n +Output: C = A · B ∈ Rn×n + +1 if (A i B nie są kwadratowe lub dim(A)̸ = dim(B)) then +2 return błąd +3 n ← dim(A) +4 if n = 1 then +5 return [[A11 · B11]] +6 m ← NextPowerOf2(n) +7 if m = n then +8 return StrassenRec(A, B, n) +9 A′ ← Pad(A, m) +10 B′ ← Pad(B, m) +11 C′ ← StrassenRec(A′, B′, m) +12 return Unpad(C′, n) +``` + +## Połączenie metod + +### Pseudokod + +``` +if SIZE<=2 { + for (i = 0; i < SIZE; i++) { + for (j = 0; j < SIZE; j++) { + for (k = 0; k < SIZE; k++) { + s = s + m_1[i][k]*m_2[k][j]; + } + res[i][j] = s; + s = 0; + } + } + return res; +} else { + k = SIZE / 2 + + # Podział macierzy na bloki + podziel m_1 na: A11, A12, A21, A22 (każda k x k) + podziel m_2 na: B11, B12, B21, B22 (każda k x k) + + # Rekurencyjne obliczenie 7 iloczynów + M1 = Strassen(A11 + A22, B11 + B22) + M2 = Strassen(A21 + A22, B11) + M3 = Strassen(A11, B12 - B22) + M4 = Strassen(A22, B21 - B11) + M5 = Strassen(A11 + A12, B22) + M6 = Strassen(A21 - A11, B11 + B12) + M7 = Strassen(A12 - A22, B21 + B22) + + # Obliczenie bloków wynikowych + C11 = M1 + M4 - M5 + M7 + C12 = M3 + M5 + C21 = M2 + M4 + C22 = M1 - M2 + M3 + M6 + + # Złożenie macierzy wynikowej + wstaw C11, C12, C21, C22 do odpowiednich ćwiartek macierzy res + + return res; +} +``` + +## Porównanie Metod + + + + + + + + +Na podstawie przedstawionych wykresów można zauważyć, że dla badanych rozmiarów macierzy szybsze jest mnożenie tradycyjne. Jednocześnie widoczna jest tendencja wskazująca, że wraz ze wzrostem n algorytm Strassena powinien stać się korzystniejszy, a dla bardzo dużych n może wykonywać się szybciej od podejścia klasycznego. Oba algorytmy posiadają zbliżoną złożoność obliczeniową. + +--- + +Wykonanie: + +* Język: Python3 +* Podział zadań: + * Tradycyjne mnożenie macierzy, testy, analizy - Maja Byrecka + * Metoda Strassena, wykresy, analizy - Michał Kowalczyk diff --git a/reports/resources/matrix-multiplication/code.txt b/reports/resources/matrix-multiplication/code.txt new file mode 100644 index 0000000..2b1f8a0 --- /dev/null +++ b/reports/resources/matrix-multiplication/code.txt @@ -0,0 +1,363 @@ + +========== matrix_multiplication_test.py ========== +from __future__ import annotations + +import pytest # noqa: F401 + +from matrix_calculus.matrix_multiplier import MatrixMultiplier + + +def test_traditional_multiplication(): + + # Test matrices with invalid dimensions + with pytest.raises(ValueError): + MatrixMultiplier.traditional_multiplication([[1]], [[2, 2], [2, 2]]) + + # Test 1x1 matrices + matrix_1 = [[1]] + matrix_2 = [[4]] + + res = MatrixMultiplier.traditional_multiplication(matrix_1, matrix_2) + assert res == [[4]] + + # Test 1xN matrices + matrix_1 = [[1, 2, 3]] + matrix_2 = [[4], [5], [6]] + + res = MatrixMultiplier.traditional_multiplication(matrix_1, matrix_2) + assert res == [[32]] + + res = MatrixMultiplier.traditional_multiplication(matrix_2, matrix_1) + assert res == [[4, 8, 12], [5, 10, 15], [6, 12, 18]] + + # Test NxM matrices + matrix_1 = [[1, 2, 3], [4, 5, 6]] + matrix_2 = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]] + + res = MatrixMultiplier.traditional_multiplication(matrix_1, matrix_2) + assert res == [[38, 44, 50, 56], [83, 98, 113, 128]] + + +def test_strassen_multiplication(): + + # Test matrices with invalid dimensions + with pytest.raises(ValueError): + MatrixMultiplier.strassen_matrix_multiplication( + [[1]], + [[2, 2], [2, 2]], + ) + + # Test if non-square matrices are rejected + with pytest.raises(ValueError): + MatrixMultiplier.strassen_matrix_multiplication( + [[1, 2, 3], [1, 2, 3]], + [[1, 2, 3], [1, 2, 3]], + ) + + # Test 1x1 matrices + matrix_1 = [[1]] + matrix_2 = [[4]] + + res = MatrixMultiplier.strassen_matrix_multiplication(matrix_1, matrix_2) + assert res == [[4]] + + # Test 2x2 matrices + matrix_1 = [[1, 2], [3, 4]] + matrix_2 = [[5, 6], [7, 8]] + expected = [[19, 22], [43, 50]] + assert expected == ( + MatrixMultiplier.strassen_matrix_multiplication( + matrix_1, + matrix_2, + ) + ) + + # Test 3x3 matrices + matrix_1 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + matrix_2 = [[9, 8, 7], [6, 5, 4], [3, 2, 1]] + expected = [[30, 24, 18], [84, 69, 54], [138, 114, 90]] + assert expected == ( + MatrixMultiplier.strassen_matrix_multiplication( + matrix_1, + matrix_2, + ) + ) + + # Test 5x5 matrices + matrix_1 = [ + [1, 2, 3, 4, 5], + [6, 7, 8, 9, 10], + [11, 12, 13, 14, 15], + [16, 17, 18, 19, 20], + [21, 22, 23, 24, 25], + ] + matrix_2 = [ + [25, 24, 23, 22, 21], + [20, 19, 18, 17, 16], + [15, 14, 13, 12, 11], + [10, 9, 8, 7, 6], + [5, 4, 3, 2, 1], + ] + expected = [ + [175, 160, 145, 130, 115], + [550, 510, 470, 430, 390], + [925, 860, 795, 730, 665], + [1300, 1210, 1120, 1030, 940], + [1675, 1560, 1445, 1330, 1215], + ] + assert expected == ( + MatrixMultiplier.strassen_matrix_multiplication( + matrix_1, + matrix_2, + ) + ) + + +========== 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]] diff --git a/reports/resources/matrix-multiplication/report.pdf b/reports/resources/matrix-multiplication/report.pdf new file mode 100644 index 0000000..ebc305c Binary files /dev/null and b/reports/resources/matrix-multiplication/report.pdf differ