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