diff --git a/python_pkg/praca_magisterska_video/answers/2/pytanie_02.md b/python_pkg/praca_magisterska_video/answers/2/pytanie_02.md
new file mode 100644
index 0000000..c1440be
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/answers/2/pytanie_02.md
@@ -0,0 +1,268 @@
+## PYTANIE 2: Algorytmy najkrótszej ścieżki (AISDI)
+
+**Omówić i porównać algorytmy: Dijkstry, Bellmana-Forda, A\*.**
+
+---
+
+### Tło pojęciowe — słowniczek
+
+**Graf** — struktura danych składająca się z **wierzchołków** (vertices/nodes) połączonych **krawędziami** (edges). Np. mapa miast: miasta = wierzchołki, drogi = krawędzie.
+
+**Wierzchołek (vertex, node)** — punkt w grafie. Oznaczany jako v, u, n, m itp. **V** = zbiór wszystkich wierzchołków; |V| = ich liczba.
+
+**Krawędź (edge)** — połączenie między dwoma wierzchołkami. **E** = zbiór krawędzi; |E| = ich liczba. Krawędź może być skierowana (A→B ≠ B→A) lub nieskierowana (A↔B).
+
+**Waga (weight)** — liczba przypisana do krawędzi, oznaczająca „koszt" przejścia. Np. odległość w km, czas podróży, opłata za przejazd. Graf z wagami = graf ważony.
+
+**Koszt (cost)** — ogólne pojęcie „ceny" przejścia ścieżką. Koszt ścieżki = suma wag krawędzi na tej ścieżce. Cel algorytmów: znaleźć ścieżkę o **minimalnym koszcie**.
+
+**SSSP (Single-Source Shortest Path)** — problem: mając JEDEN wierzchołek startowy (źródło), znajdź najkrótsze ścieżki do WSZYSTKICH pozostałych wierzchołków. Dijkstra i Bellman-Ford rozwiązują SSSP. **Single-Pair** — prostszy problem: znajdź najkrótszą ścieżkę z A do B (jednej konkretnej pary). A\* rozwiązuje Single-Pair.
+
+**d[v]** — tablica odległości. **d** = tablica (array), **v** = wierzchołek. d[v] przechowuje aktualnie najlepsze znane oszacowanie odległości od źródła do wierzchołka v. Na początku d[start] = 0, d[wszystko inne] = ∞. Algorytm stopniowo poprawia te wartości.
+
+**Zachłanny (greedy)** — strategia algorytmiczna: w każdym kroku wybierz opcję, która TERAZ wygląda najlepiej (lokalnie optymalna), bez cofania się. Dijkstra jest zachłanny: zawsze bierze wierzchołek o najmniejszym d[v] i nigdy go nie rewiduje.
+
+**Relaksacja krawędzi (edge relaxation)** — kluczowa operacja. Sprawdza: „czy droga do v przez u jest krótsza niż dotychczas znana?" Jeśli d[u] + waga(u,v) < d[v], to zaktualizuj d[v]. Nazwa od „rozluźniania" — górne ograniczenie na odległość się „rozluźnia" (maleje) w stronę prawdziwej wartości.
+
+**Tablica (array)** — najprostsza struktura danych: ciągły blok pamięci. W Dijkstrze z tablicą: szukanie minimum d[v] wymaga przejrzenia WSZYSTKICH wierzchołków → O(V) na szukanie × V razy = **O(V²)**.
+
+Przykład — graf z 4 wierzchołkami (A, B, C, D), start = A:
+
+
+
+ d = [ A:0, B:∞, C:∞, D:∞ ] ← tablica na starcie
+ odwiedzone = {}
+
+ Krok 1: przeszukaj CAŁĄ tablicę d → min = A (0)
+ d = [ A:0, B:2, C:4, D:∞ ] odw = {A}
+ ↑ ↑
+ A→B=2 A→C=4 (relaksacja sąsiadów A)
+
+ Krok 2: przeszukaj CAŁĄ tablicę d (poza odw.) → min = B (2)
+ d = [ A:0, B:2, C:4, D:5 ] odw = {A,B}
+ ↑
+ B→D=2+3=5 (relaksacja)
+
+ Krok 3: przeszukaj tablicę → min = C (4)
+ d = [ A:0, B:2, C:4, D:5 ] odw = {A,B,C}
+ ↑
+ C→D=4+5=9 > 5, nie zmieniaj
+
+ Krok 4: min = D (5). Koniec! d = [A:0, B:2, C:4, D:5]
+
+ Każdy krok = przejrzyj V elementów → 4 kroki × 4 elementy = 16 operacji = O(V²)
+
+**Kopiec (heap)** — drzewiasta struktura danych, w której element minimalny jest zawsze na szczycie. Wyciąganie minimum: O(log n). W Dijkstrze z kopcem: szukanie min d[v] to O(log V) zamiast O(V) → **O((V+E) log V)**.
+
+Przykład — ten sam graf, ale z kopcem (min-heap):
+
+ Kopiec na starcie: (0,A) ← min zawsze na szczycie
+ (reszta to ∞)
+
+ Krok 1: pop (0,A) — O(log 4)=O(2), relaksuj sąsiadów:
+ push (2,B), push (4,C)
+
+ Kopiec: (2,B)
+ / \
+ (4,C) ...
+
+ Krok 2: pop (2,B) — O(log 4), relaksuj:
+ push (5,D)
+
+ Kopiec: (4,C)
+ /
+ (5,D)
+
+ Krok 3: pop (4,C) — O(log 4). C→D: 9 > 5, nie zmieniaj.
+ Krok 4: pop (5,D) — O(log 4). Koniec!
+
+ Każdy pop = O(log V), każdy push = O(log V)
+ V popów + E pushów = O((V+E) log V)
+
+**Kopiec Fibonacciego** — zaawansowany kopiec, w którym operacja „zmniejsz klucz" (decrease-key) działa w zamortyzowanym O(1) zamiast O(log V). Dijkstra robi decrease-key dla każdej krawędzi → z kopcem Fib: **O(V log V + E)** — E operacji po O(1) + V wyciągnięć po O(log V).
+
+Przykład — kluczowa różnica: decrease-key:
+
+ Zwykły kopiec — gdy znajdziesz krótszą drogę do D:
+ d[D] zmienia się z 9 na 5
+ Trzeba „naprawić" kopiec: przesuwaj D w górę → O(log V)
+
+ Kopiec Fibonacciego — ta sama sytuacja:
+ d[D] zmienia się z 9 na 5
+ Po prostu odetnij D od rodzica i wstaw do listy korzeni → O(1)!
+ (naprawienie struktury odłożone na później — „zamortyzowane")
+
+ Różnica ma znaczenie przy GĘSTYCH grafach (E >> V):
+ - Zwykły kopiec: E × O(log V) = O(E log V) na decrease-key
+ - Kopiec Fib: E × O(1) = O(E) na decrease-key
+ Razem: O(V log V) [pop] + O(E) [decrease-key] = O(V log V + E)
+
+**Złożoność — dlaczego takie wartości:**
+
+- **O(V²)** z tablicą: V razy szukaj minimum (O(V) każdy) = V × V.
+- **O((V+E) log V)** z kopcem: V wyciągnięć min (O(log V)) + E relaksacji z decrease-key (O(log V)).
+- **O(V log V + E)** z kopcem Fib: V wyciągnięć min (O(log V)) + E decrease-key (O(1) zamortyzowane).
+
+**Programowanie dynamiczne (DP)** — technika rozwiązywania problemów przez rozbicie na mniejsze podproblemy i zapamiętywanie wyników (żeby nie liczyć tego samego dwa razy). Bellman-Ford jest DP: podproblem = „najkrótsza ścieżka do v używająca ≤ k krawędzi"; rozwiązuje dla k = 1, 2, ..., V−1.
+
+**Cykl** — ścieżka w grafie, która wraca do punktu wyjścia (A → B → C → A). **Cykl ujemny** — cykl, w którym suma wag < 0. Problem: za każdym obejściem cyklu „odległość" maleje — można iść w nieskończoność → najkrótsza ścieżka nie istnieje (= −∞).
+
+**Dlaczego O(V·E) w Bellman-Ford:** Algorytm wykonuje |V|−1 iteracji (bo najdłuższa najkrótsza ścieżka bez cykli ma co najwyżej V−1 krawędzi). W każdej iteracji relaksuje WSZYSTKIE |E| krawędzi. Razem: (V−1) × E ≈ O(V·E).
+
+**Heurystyczny** — wykorzystujący przybliżone oszacowanie (heurystykę) zamiast dokładnych obliczeń. A\* jest heurystyczny: używa funkcji h(n) do zgadywania „ile jeszcze do celu".
+
+**f(n), g(n), h(n) — co oznacza n i każda funkcja:**
+
+- **n** = aktualnie rozpatrywany wierzchołek.
+- **g(n)** = dotychczasowy koszt dotarcia od startu do n (znany, dokładny).
+- **h(n)** = heurystyka: OSZACOWANIE kosztu od n do celu (przybliżone, „zgadywane"). Np. odległość w linii prostej do celu.
+- **f(n) = g(n) + h(n)** = oszacowanie CAŁKOWITEGO kosztu ścieżki przez n. A\* zawsze rozwija wierzchołek o najniższym f(n).
+
+**Dopuszczalna (admissible)** — heurystyka h jest dopuszczalna, jeśli NIGDY nie przeszacowuje: h(n) ≤ prawdziwy koszt od n do celu. Gwarantuje, że A\* znajdzie optymalną ścieżkę. Np. odległość w linii prostej jest dopuszczalna (nie da się dojechać krócej niż prosto).
+
+**Rzeczywisty koszt** — prawdziwa najkrótsza odległość (nie oszacowanie). Np. faktyczna najkrótsza droga od n do celu, uwzględniając wszystkie krawędzie.
+
+**n → cel** — od wierzchołka n do wierzchołka docelowego (cel = destination = target).
+
+**Spójna (consistent / monotone)** — silniejszy warunek na heurystykę: h(n) ≤ w(n,m) + h(m) dla każdej krawędzi n→m. Tu **w(n,m)** = waga krawędzi z n do m, a **m** = sąsiad n. Spójność oznacza: oszacowanie z n nie jest „dużo lepsze" niż to co uzyskasz idąc jeden krok do m. Spójna ⇒ dopuszczalna (ale nie odwrotnie).
+
+**Dlaczego O(V) w najlepszym przypadku A\*:** Jeśli heurystyka jest idealna (h(n) = prawdziwy koszt), A* idzie prosto do celu, nie eksplorując zbędnych wierzchołków — odwiedza tylko te na optymalnej ścieżce ≈ O(V) jeśli ścieżka krótka. **Najgorszy przypadek** = h(n) = 0 dla wszystkich n → A* degeneruje się do Dijkstry.
+
+### Pseudokod (Python)
+
+**Dijkstra** (graph = słownik sąsiedztwa, np. `{'A': [('B',2), ('C',4)]}`):
+
+ def dijkstra(graph, source):
+ dist = {v: float('inf') for v in graph}
+ dist[source] = 0
+ visited = set()
+ for _ in range(len(graph)):
+ current = None # szukaj nieodwiedzonego wierzchołka o min dist — O(V)
+ for v in graph:
+ if v not in visited and (current is None or dist[v] < dist[current]):
+ current = v
+ if dist[current] == float('inf'):
+ break # reszta nieosiągalna
+ visited.add(current) # zamknij — NIE wracamy (zachłanność)
+ for neighbor, weight in graph[current]: # relaksacja sąsiadów
+ if dist[current] + weight < dist[neighbor]:
+ dist[neighbor] = dist[current] + weight
+ return dist # O(V²) z tablicą
+
+
+
+**Bellman-Ford** (vertices = lista wierzchołków, edges = lista krotek (src, dst, weight)):
+
+ def bellman_ford(vertices, edges, source):
+ dist = {v: float('inf') for v in vertices}
+ dist[source] = 0
+ for _ in range(len(vertices) - 1): # V−1 iteracji (najdłuższa ścieżka = V−1 krawędzi)
+ for src, dst, weight in edges: # relaksuj WSZYSTKIE krawędzie
+ if dist[src] + weight < dist[dst]:
+ dist[dst] = dist[src] + weight
+ for src, dst, weight in edges: # V-ta iteracja: wykrywanie cyklu ujemnego
+ if dist[src] + weight < dist[dst]:
+ return None # cykl ujemny!
+ return dist # O(V·E)
+
+Przykład — graf z ujemnymi wagami (Dijkstra daje ZŁY wynik, B-F poprawny):
+
+ Graf: S→A(2), A→C(3), S→B(5), B→A(−4)
+
+ Dijkstra:
+ 1. S(0): dist[A]=2, dist[B]=5
+ 2. A(2) zamknięty: dist[C]=5
+ 3. B(5): B→A = 5−4 = 1 < 2, ALE A już zamknięty → POMIJA!
+ Wynik: A=2, C=5 ← BŁĄD (prawidłowe: A=1, C=4)
+
+ Bellman-Ford — relaksuje WSZYSTKIE krawędzie, V−1 = 3 razy:
+ Start: dist = [S:0, A:∞, B:∞, C:∞]
+
+ Iteracja 1:
+ S→A: 0+2=2 < ∞ → A=2
+ A→C: 2+3=5 < ∞ → C=5
+ S→B: 0+5=5 < ∞ → B=5
+ B→A: 5−4=1 < 2 → A=1 ← ujemna waga poprawia!
+
+ Iteracja 2:
+ A→C: 1+3=4 < 5 → C=4 ← propagacja poprawionego A
+
+ Iteracja 3: brak zmian → stabilne.
+ Wynik: [S:0, A:1, B:5, C:4] ← POPRAWNE
+
+ Wykrywanie cyklu ujemnego — dodaj krawędź C→B(−3):
+ Cykl B→A→C→B = −4 + 3 + (−3) = −4 < 0.
+ Po V−1 iteracjach dist nadal maleje → V-ta iteracja:
+ dist[src] + weight < dist[dst] → return None
+
+
+
+
+
+
+
+**A\*** (graph jak Dijkstra; heuristic = h(v) → oszacowanie odl. do celu):
+
+ def a_star(graph, source, goal, heuristic):
+ cost_so_far = {source: 0} # g(n) — faktyczny koszt dotarcia
+ priority = {source: heuristic(source)} # f(n) = g(n) + h(n)
+ came_from = {} # do odtworzenia ścieżki
+ visited = set()
+ while priority:
+ current = min(priority, key=priority.get) # wierzchołek o min f(n)
+ del priority[current]
+ if current == goal:
+ break # dotarliśmy — A* kończy (Dijkstra przetworzyłby wszystko)
+ visited.add(current)
+ for neighbor, weight in graph[current]:
+ if neighbor in visited:
+ continue
+ new_cost = cost_so_far[current] + weight
+ if neighbor not in cost_so_far or new_cost < cost_so_far[neighbor]:
+ cost_so_far[neighbor] = new_cost
+ priority[neighbor] = new_cost + heuristic(neighbor)
+ came_from[neighbor] = current
+ return came_from, cost_so_far.get(goal) # ścieżka + koszt
+
+
+
+---
+
+### Dijkstra — zachłanny, SSSP
+
+**Ograniczenie:** wagi ≥ 0.
+**Idea:** Relaksacja krawędzi; zawsze przetwarzaj wierzchołek o najmniejszym d[v].
+**Złożoność:** O(V²) z tablicą, O((V+E) log V) z kopcem, O(V log V + E) z kopcem Fibonacciego.
+**Dlaczego nie ujemne wagi?** Raz oznaczony wierzchołek nie jest rewidowany — ujemna krawędź może go poprawić.
+
+### Bellman-Ford — programowanie dynamiczne, SSSP
+
+**Zaleta:** obsługuje ujemne wagi + **wykrywa cykle ujemne**.
+**Idea:** |V|−1 iteracji relaksacji WSZYSTKICH krawędzi. Jeśli w iteracji V nadal można poprawić → cykl ujemny.
+**Złożoność:** O(V·E) — zawsze.
+
+### A\* — heurystyczny, Single-Pair
+
+**Rozszerzenie Dijkstry:** f(n) = g(n) + h(n), gdzie h(n) to heurystyka.
+**Wymóg:** h dopuszczalna (admissible): h(n) ≤ rzeczywisty koszt n→cel. Jeśli h spójna (consistent): h(n) ≤ w(n,m) + h(m) — optymalne.
+**Złożoność:** zależy od h; najlepszy przypadek O(V), najgorszy jak Dijkstra.
+
+### Porównanie
+
+| Cecha | Dijkstra | Bellman-Ford | A\* |
+| -------------- | ------------- | ---------------- | ------------ |
+| Typ | Zachłanny | Prog. dynamiczne | Heurystyczny |
+| Problem | SSSP | SSSP | Single-pair |
+| Ujemne wagi | NIE | TAK | NIE |
+| Wykrywa cykle- | NIE | TAK | NIE |
+| Złożoność | O((V+E)log V) | O(VE) | Zależy od h |
+
+### Etymologia
+
+**Dijkstra** — Edsger W. Dijkstra (Holandia, 1959); pionier informatyki (Turing Award 1972). **Bellman-Ford** — Richard Bellman (twórca programowania dynamicznego) + Lester Ford Jr. (1956). **A\*** — Hart, Nilsson, Raphael (Stanford, 1968); „A\*" = ulepszona wersja algorytmu „A". **Zachłanny (Greedy)** — algorytm „chciwie" bierze lokalnie najlepszą opcję. **SSSP** — Single-Source Shortest Path. **Programowanie dynamiczne** — Bellman wybrał „dynamic" by brzmiało imponująco dla polityków (nie miało związku z dynamiką!). **Heurystyka** — grec. „heuriskein" = znajdować (to samo co „Eureka!" Archimedesa). **Relaksacja** — „rozluźnianie" górnego ograniczenia na odległość d[v].
+
+### Jak zapamiętać
+
+- **Dijkstra = chciwy**, bierze minimum — ale „nie patrzy wstecz" (stąd problem z ujemnymi wagami)
+- **Bellman-Ford = brute force x (V−1)** — relaksuj wszystko, V−1 razy, bo najdłuższa ścieżka ma V−1 krawędzi
+- **A\* = Dijkstra + „GPS"** — heurystyka mówi w którą stronę jest cel
diff --git a/python_pkg/praca_magisterska_video/generate_images/anki_approach_1.py b/python_pkg/praca_magisterska_video/generate_images/anki_approach_1.py
new file mode 100755
index 0000000..5031538
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/anki_approach_1.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+"""Approach 1: STRICT FILTERING ONLY.
+
+- Only include cards with answers > 100 characters
+- No changes to extraction logic.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+import re
+
+
+def clean_text(text) -> str:
+ """Clean text."""
+ if not text:
+ return ""
+ text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
+ text = re.sub(r"(?\1", text)
+ text = text.replace("\t", " ")
+ text = text.replace('"', """)
+ text = re.sub(r" +", " ", text)
+ return text.strip()
+
+
+def extract_cards(filepath) -> list[dict[str, str]]:
+ """Extract cards."""
+ with Path(filepath).open(encoding="utf-8") as f:
+ content = f.read()
+
+ cards = []
+ filename = Path(filepath).name
+ match = re.match(r"(\d+)-(.+)\.md", filename)
+ num = match.group(1) if match else "00"
+
+ subj_match = re.search(r"Przedmiot:\s*(\w+)", content)
+ subject = subj_match.group(1) if subj_match else "Ogólne"
+ base_tags = f"egzamin pyt{num} {subject}"
+
+ # Main question
+ q_match = re.search(
+ r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
+ )
+ if q_match:
+ main_q = re.sub(r"\s+", " ", q_match.group(1).strip())
+
+ # Simple extraction - headers as answer
+ answer_match = re.search(
+ r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## |\Z)", content, re.DOTALL
+ )
+ if answer_match:
+ headers = re.findall(
+ r"^### (?:\d+\.\s*)?(.+)$", answer_match.group(1), re.MULTILINE
+ )
+ if headers:
+ answer = (
+ "
"
+ + "".join([f"- {clean_text(h)}
" for h in headers[:6]])
+ + "
"
+ )
+ cards.append(
+ {"front": clean_text(main_q), "back": answer, "tags": base_tags}
+ )
+
+ # Detail cards - simple extraction
+ sections = re.findall(
+ r"^### (?:\d+\.\s*)?([^\n]+)\n((?:(?!^### ).)*)",
+ content,
+ re.MULTILINE | re.DOTALL,
+ )
+ for header, body in sections:
+ header = header.strip()
+ body = body.strip()
+ if len(body) < 50:
+ continue
+
+ # Get first paragraph
+ paras = [
+ p.strip()
+ for p in body.split("\n\n")
+ if p.strip() and not p.startswith("```")
+ ]
+ if paras:
+ answer = clean_text(paras[0][:400])
+ cards.append(
+ {
+ "front": f"Wyjaśnij: {clean_text(header)}",
+ "back": answer,
+ "tags": base_tags,
+ }
+ )
+
+ return cards
+
+
+def main() -> None:
+ """Main."""
+ odpowiedzi_dir = Path("/home/kuchy/praca_magisterska/pytania/odpowiedzi")
+ output_file = Path("/home/kuchy/praca_magisterska/pytania/anki_1_strict_filter.txt")
+
+ all_cards = []
+ for md_file in sorted(odpowiedzi_dir.glob("*.md")):
+ all_cards.extend(extract_cards(md_file))
+
+ # APPROACH 1: Strict filtering - only cards with answer > 100 chars
+ filtered_cards = [c for c in all_cards if len(c["back"]) > 100]
+
+ # Remove duplicates
+ seen = set()
+ unique = []
+ for c in filtered_cards:
+ if c["front"][:80] not in seen:
+ seen.add(c["front"][:80])
+ unique.append(c)
+
+ with Path(output_file).open("w", encoding="utf-8") as f:
+ f.write(
+ "#separator:Tab\n#html:true\n#notetype:Basic\n#deck:Egzamin_1_StrictFilter\n\n"
+ )
+ for c in unique:
+ f.write(f"{c['front']}\t{c['back']}\t{c['tags']}\n")
+
+ print(f"✅ Approach 1 (Strict Filter): {len(unique)} cards -> {output_file.name}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/python_pkg/praca_magisterska_video/generate_images/anki_approach_2.py b/python_pkg/praca_magisterska_video/generate_images/anki_approach_2.py
new file mode 100755
index 0000000..98d459b
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/anki_approach_2.py
@@ -0,0 +1,152 @@
+#!/usr/bin/env python3
+"""Approach 2: BETTER EXTRACTION ONLY.
+
+- Improved algorithm to get more complete content
+- No minimum length filtering.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+import re
+
+
+def clean_text(text) -> str:
+ """Clean text."""
+ if not text:
+ return ""
+ text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
+ text = re.sub(r"(?\1", text)
+ text = text.replace("\t", " ")
+ text = text.replace('"', """)
+ text = re.sub(r" +", " ", text)
+ return text.strip()
+
+
+def extract_structured_content(body) -> str | None:
+ """Better extraction - look for multiple content types."""
+ parts = []
+
+ # 1. Look for definitions
+ def_match = re.search(r"#### Definicja[^\n]*\n([^\n#]+)", body)
+ if def_match:
+ parts.append(f"Definicja: {def_match.group(1).strip()}")
+
+ # 2. Look for bullet points with bold terms
+ bullets = re.findall(r"[-•]\s*\*\*([^*]+)\*\*[:\s-]*([^\n]*)", body)
+ for term, desc in bullets[:5]:
+ if desc.strip():
+ parts.append(f"• {term}: {desc.strip()}")
+ else:
+ parts.append(f"• {term}")
+
+ # 3. Look for key-value patterns
+ if not parts:
+ kvs = re.findall(r"\*\*([^*]+)\*\*\s*[-:]\s*([^\n*]+)", body)
+ for k, v in kvs[:4]:
+ parts.append(f"{k}: {v.strip()}")
+
+ # 4. Get paragraphs as fallback
+ if not parts:
+ paras = [
+ p.strip()
+ for p in body.split("\n\n")
+ if p.strip()
+ and not p.startswith("```")
+ and not p.startswith("|")
+ and len(p.strip()) > 30
+ ]
+ for p in paras[:2]:
+ parts.append(p[:300])
+
+ return "
".join([clean_text(p) for p in parts]) if parts else None
+
+
+def extract_cards(filepath) -> list[dict[str, str]]:
+ """Extract cards."""
+ with Path(filepath).open(encoding="utf-8") as f:
+ content = f.read()
+
+ cards = []
+ filename = Path(filepath).name
+ match = re.match(r"(\d+)-(.+)\.md", filename)
+ num = match.group(1) if match else "00"
+
+ subj_match = re.search(r"Przedmiot:\s*(\w+)", content)
+ subject = subj_match.group(1) if subj_match else "Ogólne"
+ base_tags = f"egzamin pyt{num} {subject}"
+
+ # Main question with better extraction
+ q_match = re.search(
+ r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
+ )
+ if q_match:
+ main_q = re.sub(r"\s+", " ", q_match.group(1).strip())
+
+ answer_match = re.search(
+ r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## |\Z)", content, re.DOTALL
+ )
+ if answer_match:
+ answer = extract_structured_content(answer_match.group(1))
+ if answer:
+ cards.append(
+ {"front": clean_text(main_q), "back": answer, "tags": base_tags}
+ )
+
+ # Detail cards with better extraction
+ sections = re.findall(
+ r"^### (?:\d+\.\s*)?([^\n]+)\n((?:(?!^### ).)*)",
+ content,
+ re.MULTILINE | re.DOTALL,
+ )
+ for header, body in sections:
+ header = header.strip()
+ if "Przykład" in header or '"' in header or len(body) < 50:
+ continue
+
+ answer = extract_structured_content(body)
+ if answer:
+ cards.append(
+ {
+ "front": f"Wyjaśnij: {clean_text(header)}",
+ "back": answer,
+ "tags": base_tags,
+ }
+ )
+
+ return cards
+
+
+def main() -> None:
+ """Main."""
+ odpowiedzi_dir = Path("/home/kuchy/praca_magisterska/pytania/odpowiedzi")
+ output_file = Path(
+ "/home/kuchy/praca_magisterska/pytania/anki_2_better_extract.txt"
+ )
+
+ all_cards = []
+ for md_file in sorted(odpowiedzi_dir.glob("*.md")):
+ all_cards.extend(extract_cards(md_file))
+
+ # No filtering - just dedupe
+ seen = set()
+ unique = []
+ for c in all_cards:
+ if c["front"][:80] not in seen:
+ seen.add(c["front"][:80])
+ unique.append(c)
+
+ with Path(output_file).open("w", encoding="utf-8") as f:
+ f.write(
+ "#separator:Tab\n#html:true\n#notetype:Basic\n#deck:Egzamin_2_BetterExtract\n\n"
+ )
+ for c in unique:
+ f.write(f"{c['front']}\t{c['back']}\t{c['tags']}\n")
+
+ print(
+ f"✅ Approach 2 (Better Extraction): {len(unique)} cards -> {output_file.name}"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/python_pkg/praca_magisterska_video/generate_images/anki_generator.py b/python_pkg/praca_magisterska_video/generate_images/anki_generator.py
new file mode 100755
index 0000000..a62f6c6
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/anki_generator.py
@@ -0,0 +1,426 @@
+#!/usr/bin/env python3
+"""Anki Generator - Modular approach with 3 combinable strategies.
+
+Usage:
+ python anki_generator.py [options]
+
+Options:
+ --filter Apply strict filtering (answers > 100 chars)
+ --extract Use improved extraction algorithm
+ --main-only Only generate main exam questions (45 comprehensive cards)
+
+Combinations:
+ python anki_generator.py # Basic extraction, no filter
+ python anki_generator.py --filter # Approach 1: Strict filter only
+ python anki_generator.py --extract # Approach 2: Better extraction only
+ python anki_generator.py --main-only # Approach 3: Main questions only
+ python anki_generator.py --filter --extract # Approach 4: Filter + Better extraction
+ python anki_generator.py --filter --main-only # Approach 5: Filter + Main only
+ python anki_generator.py --extract --main-only # Approach 6: Better extraction + Main only
+ python anki_generator.py --filter --extract --main-only # Approach 7: All three
+"""
+
+from __future__ import annotations
+
+import argparse
+from pathlib import Path
+import re
+
+# =============================================================================
+# SHARED UTILITIES
+# =============================================================================
+
+
+def clean_text(text) -> str:
+ """Clean and format text for Anki."""
+ if not text:
+ return ""
+ text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
+ text = re.sub(r"(?\1", text)
+ text = text.replace("\t", " ")
+ text = text.replace('"', """)
+ text = re.sub(r" +", " ", text)
+ return text.strip()
+
+
+def get_file_metadata(filepath) -> tuple[str, str, str]:
+ """Extract question number and subject from filename."""
+ filename = Path(filepath).name
+ match = re.match(r"(\d+)-(.+)\.md", filename)
+ num = match.group(1) if match else "00"
+
+ with Path(filepath).open(encoding="utf-8") as f:
+ content = f.read()
+
+ subj_match = re.search(r"Przedmiot:\s*(\w+)", content)
+ subject = subj_match.group(1) if subj_match else "Ogólne"
+
+ return num, subject, content
+
+
+def get_main_question(content) -> str | None:
+ """Extract the main exam question."""
+ q_match = re.search(
+ r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
+ )
+ if q_match:
+ return re.sub(r"\s+", " ", q_match.group(1).strip())
+ return None
+
+
+# =============================================================================
+# APPROACH 1: STRICT FILTERING
+# =============================================================================
+
+
+def apply_strict_filter(cards, min_length=100) -> list[dict[str, str]]:
+ """Filter cards to only include those with answers > min_length characters."""
+ return [c for c in cards if len(c["back"]) > min_length]
+
+
+# =============================================================================
+# APPROACH 2: BETTER EXTRACTION
+# =============================================================================
+
+
+def extract_structured_content(body) -> str | None:
+ """Improved extraction - multiple content types with better formatting."""
+ parts = []
+
+ # 1. Definitions
+ def_match = re.search(r"#### Definicja[^\n]*\n([^\n#]+)", body)
+ if def_match:
+ parts.append(f"Definicja: {def_match.group(1).strip()}")
+
+ # 2. Bullet points with bold terms
+ bullets = re.findall(r"[-•]\s*\*\*([^*]+)\*\*[:\s-]*([^\n]*)", body)
+ for term, desc in bullets[:5]:
+ if desc.strip():
+ parts.append(f"• {term}: {desc.strip()}")
+ else:
+ parts.append(f"• {term}")
+
+ # 3. Key-value patterns
+ if len(parts) < 2:
+ kvs = re.findall(r"\*\*([^*\n]+)\*\*\s*[--:]\s*([^\n*]{10,150})", body)
+ for k, v in kvs[:4]:
+ entry = f"{k.strip()}: {v.strip()}"
+ if entry not in parts:
+ parts.append(entry)
+
+ # 4. Paragraphs as fallback
+ if not parts:
+ paras = [
+ p.strip()
+ for p in body.split("\n\n")
+ if p.strip()
+ and not p.startswith("```")
+ and not p.startswith("|")
+ and len(p.strip()) > 30
+ ]
+ for p in paras[:2]:
+ parts.append(p[:300])
+
+ return "
".join([clean_text(p) for p in parts]) if parts else None
+
+
+def extract_cards_better(filepath) -> list[dict[str, str]]:
+ """Extract cards with improved algorithm."""
+ num, subject, content = get_file_metadata(filepath)
+ base_tags = f"egzamin pyt{num} {subject}"
+ cards = []
+
+ # Main question
+ main_q = get_main_question(content)
+ if main_q:
+ answer_match = re.search(
+ r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## [^�]|\Z)", content, re.DOTALL
+ )
+ if answer_match:
+ answer = extract_structured_content(answer_match.group(1))
+ if answer:
+ cards.append(
+ {
+ "front": clean_text(main_q),
+ "back": answer,
+ "tags": f"{base_tags} main",
+ }
+ )
+
+ # Detail sections
+ sections = re.findall(
+ r"^### (?:\d+\.\s*)?([^\n]+)\n((?:(?!^### ).)*)",
+ content,
+ re.MULTILINE | re.DOTALL,
+ )
+ for header, body in sections:
+ header = header.strip()
+ if (
+ "Przykład" in header
+ or '"' in header
+ or "Mnemonic" in header
+ or len(body) < 50
+ ):
+ continue
+
+ answer = extract_structured_content(body)
+ if answer:
+ cards.append(
+ {
+ "front": f"Wyjaśnij: {clean_text(header)}",
+ "back": answer,
+ "tags": f"{base_tags} detail",
+ }
+ )
+
+ return cards
+
+
+def extract_cards_basic(filepath) -> list[dict[str, str]]:
+ """Basic extraction - simpler algorithm."""
+ num, subject, content = get_file_metadata(filepath)
+ base_tags = f"egzamin pyt{num} {subject}"
+ cards = []
+
+ # Main question - just headers
+ main_q = get_main_question(content)
+ if main_q:
+ answer_match = re.search(
+ r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## |\Z)", content, re.DOTALL
+ )
+ if answer_match:
+ headers = re.findall(
+ r"^### (?:\d+\.\s*)?(.+)$", answer_match.group(1), re.MULTILINE
+ )
+ if headers:
+ answer = (
+ ""
+ + "".join([f"- {clean_text(h)}
" for h in headers[:6]])
+ + "
"
+ )
+ cards.append(
+ {
+ "front": clean_text(main_q),
+ "back": answer,
+ "tags": f"{base_tags} main",
+ }
+ )
+
+ # Detail sections - first paragraph only
+ sections = re.findall(
+ r"^### (?:\d+\.\s*)?([^\n]+)\n((?:(?!^### ).)*)",
+ content,
+ re.MULTILINE | re.DOTALL,
+ )
+ for header, body in sections:
+ header = header.strip()
+ body = body.strip()
+ if len(body) < 50 or "Przykład" in header:
+ continue
+
+ paras = [
+ p.strip()
+ for p in body.split("\n\n")
+ if p.strip() and not p.startswith("```")
+ ]
+ if paras:
+ answer = clean_text(paras[0][:400])
+ cards.append(
+ {
+ "front": f"Wyjaśnij: {clean_text(header)}",
+ "back": answer,
+ "tags": f"{base_tags} detail",
+ }
+ )
+
+ return cards
+
+
+# =============================================================================
+# APPROACH 3: MAIN QUESTIONS ONLY
+# =============================================================================
+
+
+def extract_main_only(filepath) -> list[dict[str, str]]:
+ """Extract only the main exam question with comprehensive answer."""
+ num, subject, content = get_file_metadata(filepath)
+ base_tags = f"egzamin pyt{num} {subject} main"
+
+ main_q = get_main_question(content)
+ if not main_q:
+ return []
+
+ # Build comprehensive answer from multiple sections
+ answer_parts = []
+
+ # Get main answer section
+ answer_match = re.search(
+ r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## [^�]|\Z)", content, re.DOTALL
+ )
+ if answer_match:
+ section = answer_match.group(1)
+
+ # Get all ### headers with their first substantive content
+ headers = re.findall(
+ r"^### (?:\d+\.\s*)?([^\n]+)\n((?:(?!^### ).)*)",
+ section,
+ re.MULTILINE | re.DOTALL,
+ )
+
+ for header, body in headers[:5]:
+ header = header.strip()
+ if "Przykład" in header or "Mnemonic" in header or '"' in header:
+ continue
+
+ # Get key point from this section
+ key_point = None
+
+ # Try to get a definition or first bullet
+ def_match = re.search(
+ r"Rozpoznawana klasa języków\s*\n\s*\*\*([^*]+)\*\*", body
+ )
+ if def_match:
+ key_point = def_match.group(1).strip()
+
+ if not key_point:
+ bullets = re.findall(r"[-•]\s*\*\*([^*]+)\*\*[:\s-]*([^\n]*)", body)
+ if bullets:
+ term, desc = bullets[0]
+ key_point = f"{term}: {desc.strip()}" if desc.strip() else term
+
+ if not key_point:
+ para_match = re.search(r"\n\n([^#\n\-•|`][^\n]{20,150})", body)
+ if para_match:
+ key_point = para_match.group(1).strip()
+
+ if key_point:
+ answer_parts.append(f"{header}: {key_point}")
+
+ if answer_parts:
+ answer = "
".join([clean_text(p) for p in answer_parts])
+ return [{"front": clean_text(main_q), "back": answer, "tags": base_tags}]
+
+ return []
+
+
+# =============================================================================
+# MAIN GENERATOR
+# =============================================================================
+
+
+def generate_anki(use_filter=False, use_better_extract=False, main_only=False) -> Path:
+ """Generate Anki deck with specified approaches."""
+ odpowiedzi_dir = Path("/home/kuchy/praca_magisterska/pytania/odpowiedzi")
+
+ # Determine output filename based on options
+ suffix_parts = []
+ if use_filter:
+ suffix_parts.append("filter")
+ if use_better_extract:
+ suffix_parts.append("extract")
+ if main_only:
+ suffix_parts.append("main")
+ suffix = "_".join(suffix_parts) if suffix_parts else "basic"
+
+ output_file = Path(f"/home/kuchy/praca_magisterska/pytania/anki_{suffix}.txt")
+ deck_name = f"Egzamin_{suffix.replace('_', '+')}"
+
+ all_cards = []
+
+ for md_file in sorted(odpowiedzi_dir.glob("*.md")):
+ if main_only:
+ # Approach 3: Only main questions
+ cards = extract_main_only(md_file)
+ elif use_better_extract:
+ # Approach 2: Better extraction
+ cards = extract_cards_better(md_file)
+ else:
+ # Basic extraction
+ cards = extract_cards_basic(md_file)
+
+ all_cards.extend(cards)
+
+ # Approach 1: Apply filtering if requested
+ if use_filter:
+ all_cards = apply_strict_filter(all_cards, min_length=100)
+
+ # Remove duplicates
+ seen = set()
+ unique = []
+ for c in all_cards:
+ key = c["front"][:80]
+ if key not in seen:
+ seen.add(key)
+ unique.append(c)
+
+ # Write output
+ with Path(output_file).open("w", encoding="utf-8") as f:
+ f.write(f"#separator:Tab\n#html:true\n#notetype:Basic\n#deck:{deck_name}\n\n")
+ for c in unique:
+ f.write(f"{c['front']}\t{c['back']}\t{c['tags']}\n")
+
+ # Statistics
+ lengths = [len(c["back"]) for c in unique]
+ short = sum(1 for l in lengths if l < 50)
+ medium = sum(1 for l in lengths if 50 <= l < 150)
+ good = sum(1 for l in lengths if l >= 150)
+
+ print(f"✅ Generated: {output_file.name}")
+ print(f" Cards: {len(unique)}")
+ print(f" Quality: {short} short / {medium} medium / {good} good")
+ print()
+
+ return output_file
+
+
+def main() -> None:
+ """Main."""
+ parser = argparse.ArgumentParser(
+ description="Generate Anki flashcards with modular approaches"
+ )
+ parser.add_argument(
+ "--filter",
+ action="store_true",
+ help="Approach 1: Strict filtering (>100 chars)",
+ )
+ parser.add_argument(
+ "--extract", action="store_true", help="Approach 2: Better extraction algorithm"
+ )
+ parser.add_argument(
+ "--main-only", action="store_true", help="Approach 3: Main exam questions only"
+ )
+ parser.add_argument(
+ "--all-combinations", action="store_true", help="Generate all 7 combinations"
+ )
+
+ args = parser.parse_args()
+
+ if args.all_combinations:
+ # Generate all 7 combinations
+ print("=" * 60)
+ print("Generating all 7 combinations...")
+ print("=" * 60 + "\n")
+
+ combinations = [
+ (True, False, False), # 1: Filter only
+ (False, True, False), # 2: Extract only
+ (False, False, True), # 3: Main only
+ (True, True, False), # 4: Filter + Extract
+ (True, False, True), # 5: Filter + Main
+ (False, True, True), # 6: Extract + Main
+ (True, True, True), # 7: All three
+ ]
+
+ for i, (f, e, m) in enumerate(combinations, 1):
+ print(f"--- Combination {i} (filter={f}, extract={e}, main={m}) ---")
+ generate_anki(use_filter=f, use_better_extract=e, main_only=m)
+ else:
+ generate_anki(
+ use_filter=args.filter,
+ use_better_extract=args.extract,
+ main_only=args.main_only,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_agent_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_agent_diagrams.py
new file mode 100755
index 0000000..0e9acdf
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_agent_diagrams.py
@@ -0,0 +1,767 @@
+#!/usr/bin/env python3
+"""Generate diagrams for PYTANIE 15: Agent upostaciowiony w robotyce.
+
+Diagrams:
+ 1. See-Think-Act cycle of an embodied agent
+ 2. 3T Architecture (Planner / Sequencer / Controller)
+ 3. Behavior Tree example (robot pick-and-place)
+ 4. BDI model flow
+
+All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
+"""
+
+from __future__ import annotations
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 11
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.2,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=True,
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y), w, h, boxstyle="round,pad=0.05", lw=lw, edgecolor=LN, facecolor=fill
+ )
+ else:
+ rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+def draw_arrow(
+ ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN, label="", label_offset=0.12
+) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ )
+ if label:
+ mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_offset
+ ax.text(mx, my, label, ha="center", va="bottom", fontsize=6.5, color=color)
+
+
+def draw_dashed_arrow(
+ ax, x1, y1, x2, y2, lw=1.0, color=LN, label="", label_offset=0.12
+) -> None:
+ """Draw dashed arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": color,
+ "lw": lw,
+ "linestyle": "dashed",
+ },
+ )
+ if label:
+ mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_offset
+ ax.text(mx, my, label, ha="center", va="bottom", fontsize=6.5, color=color)
+
+
+# ─── DIAGRAM 1: See-Think-Act Cycle ──────────────────────────────
+def draw_see_think_act() -> None:
+ """Draw see think act."""
+ fig, ax = plt.subplots(1, 1, figsize=(7, 4.5), facecolor=BG)
+ ax.set_xlim(0, 7)
+ ax.set_ylim(0, 4.5)
+ ax.axis("off")
+ ax.set_title(
+ "Cykl agenta upostaciowionego: Percepcja → Deliberacja → Akcja",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Environment box (large background)
+ env_rect = FancyBboxPatch(
+ (0.2, 0.2),
+ 6.6,
+ 1.0,
+ boxstyle="round,pad=0.08",
+ lw=1.5,
+ edgecolor=LN,
+ facecolor=GRAY1,
+ linestyle="--",
+ )
+ ax.add_patch(env_rect)
+ ax.text(
+ 3.5,
+ 0.7,
+ "ŚRODOWISKO FIZYCZNE\n(przeszkody, obiekty, ludzie)",
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontstyle="italic",
+ )
+
+ # Agent body (large rounded box)
+ agent_rect = FancyBboxPatch(
+ (0.5, 1.5),
+ 6.0,
+ 2.6,
+ boxstyle="round,pad=0.1",
+ lw=2.0,
+ edgecolor=LN,
+ facecolor=GRAY4,
+ )
+ ax.add_patch(agent_rect)
+ ax.text(
+ 3.5,
+ 3.85,
+ "AGENT UPOSTACIOWIONY (robot)",
+ ha="center",
+ va="center",
+ fontsize=9,
+ fontweight="bold",
+ )
+
+ # Three main phases
+ bw = 1.4
+ bh = 0.7
+ by = 2.2
+
+ # SEE
+ draw_box(
+ ax,
+ 0.8,
+ by,
+ bw,
+ bh,
+ "SEE\n(Percepcja)",
+ fill=GRAY2,
+ fontsize=8,
+ fontweight="bold",
+ )
+ ax.text(
+ 1.5,
+ by - 0.2,
+ "kamery, LIDAR\nczujniki dotyku",
+ ha="center",
+ va="top",
+ fontsize=6,
+ fontstyle="italic",
+ )
+
+ # THINK
+ draw_box(
+ ax,
+ 2.8,
+ by,
+ bw,
+ bh,
+ "THINK\n(Deliberacja)",
+ fill=GRAY3,
+ fontsize=8,
+ fontweight="bold",
+ )
+ ax.text(
+ 3.5,
+ by - 0.2,
+ "planowanie trasy\nmodel BDI",
+ ha="center",
+ va="top",
+ fontsize=6,
+ fontstyle="italic",
+ )
+
+ # ACT
+ draw_box(
+ ax, 4.8, by, bw, bh, "ACT\n(Akcja)", fill=GRAY2, fontsize=8, fontweight="bold"
+ )
+ ax.text(
+ 5.5,
+ by - 0.2,
+ "silniki, chwytaki\nkomendy PWM",
+ ha="center",
+ va="top",
+ fontsize=6,
+ fontstyle="italic",
+ )
+
+ # Arrows between phases
+ draw_arrow(
+ ax, 0.8 + bw, by + bh / 2, 2.8, by + bh / 2, lw=1.5, label="dane sensoryczne"
+ )
+ draw_arrow(
+ ax, 2.8 + bw, by + bh / 2, 4.8, by + bh / 2, lw=1.5, label="komendy sterujące"
+ )
+
+ # Arrows to/from environment
+ draw_arrow(ax, 1.5, 1.2, 1.5, by, lw=1.3, label="odczyt", label_offset=0.08)
+ draw_arrow(ax, 5.5, by, 5.5, 1.2, lw=1.3, label="działanie", label_offset=0.08)
+
+ # Feedback loop arrow (from ACT back to environment back to SEE)
+ ax.annotate(
+ "",
+ xy=(1.5, 1.15),
+ xytext=(5.5, 1.15),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": "#555555",
+ "lw": 1.0,
+ "linestyle": "dashed",
+ "connectionstyle": "arc3,rad=-0.15",
+ },
+ )
+ ax.text(
+ 3.5,
+ 0.35,
+ "← sprzężenie zwrotne (efekt akcji zmienia środowisko) →",
+ ha="center",
+ va="center",
+ fontsize=6,
+ color="#555555",
+ )
+
+ fig.tight_layout()
+ path = str(Path(OUTPUT_DIR) / "agent_see_think_act.png")
+ fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" ✓ {path}")
+
+
+# ─── DIAGRAM 2: 3T Architecture ─────────────────────────────────
+def draw_3t_architecture() -> None:
+ """Draw 3t architecture."""
+ fig, ax = plt.subplots(1, 1, figsize=(7, 5.5), facecolor=BG)
+ ax.set_xlim(0, 7)
+ ax.set_ylim(0, 5.5)
+ ax.axis("off")
+ ax.set_title(
+ "Architektura 3T sterownika robota (3-Layer Architecture)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ layers = [
+ {
+ "y": 4.0,
+ "name": "WARSTWA 3: PLANNER\n(Deliberacja)",
+ "time": "sekundy \u2013 minuty",
+ "fill": GRAY1,
+ "example": 'Cel: "Jed\u017a do kuchni po kubek"\nPlanowanie trasy A \u2192 B \u2192 C',
+ "tech": "Planowanie symboliczne\nA*, graf zada\u0144",
+ },
+ {
+ "y": 2.6,
+ "name": "WARSTWA 2: SEQUENCER\n(Wykonawca)",
+ "time": "100 ms \u2013 sekundy",
+ "fill": GRAY2,
+ "example": "Sekwencja: Jed\u017a do drzwi \u2192\nOtw\u00f3rz \u2192 Jed\u017a do blatu \u2192 Chwy\u0107",
+ "tech": "FSM, Behavior Trees\nkoordynacja zachowa\u0144",
+ },
+ {
+ "y": 1.2,
+ "name": "WARSTWA 1: CONTROLLER\n(Reaktywny)",
+ "time": "milisekundy",
+ "fill": GRAY3,
+ "example": "PID: utrzymaj pr\u0119dko\u015b\u0107 0.5 m/s\nUnikaj kolizji (emergency stop)",
+ "tech": "PID, regulacja\nbezpo\u015brednie I/O",
+ },
+ ]
+
+ bw = 4.0
+ bh = 0.85
+
+ for _i, layer in enumerate(layers):
+ y = layer["y"]
+ # Main layer box
+ draw_box(
+ ax,
+ 0.3,
+ y,
+ bw,
+ bh,
+ layer["name"],
+ fill=layer["fill"],
+ fontsize=8,
+ fontweight="bold",
+ )
+
+ # Time label (left)
+ ax.text(
+ 0.15,
+ y + bh / 2,
+ layer["time"],
+ ha="right",
+ va="center",
+ fontsize=6,
+ fontstyle="italic",
+ rotation=0,
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "white",
+ "edgecolor": LN,
+ "lw": 0.5,
+ },
+ )
+
+ # Example (right side)
+ draw_box(ax, 4.5, y, 2.3, bh, layer["example"], fill="white", fontsize=6.5)
+
+ # Technology used
+ # ax.text(4.5 + 1.15, y - 0.12, layer["tech"], ha='center', va='top',
+ # fontsize=5.5, fontstyle='italic', color='#555555')
+
+ # Arrows between layers (downward = commands, upward = status)
+ for i in range(len(layers) - 1):
+ y_top = layers[i]["y"]
+ y_bot = layers[i + 1]["y"] + 0.85
+ # Command arrow (down)
+ draw_arrow(
+ ax, 1.8, y_top, 1.8, y_bot, lw=1.3, label="polecenia ↓", label_offset=0.02
+ )
+ # Status arrow (up)
+ draw_arrow(
+ ax,
+ 2.8,
+ y_bot,
+ 2.8,
+ y_top,
+ lw=1.0,
+ style="->",
+ color="#666666",
+ label="↑ status",
+ label_offset=0.02,
+ )
+
+ # Environment at bottom
+ env_rect = FancyBboxPatch(
+ (0.3, 0.3),
+ bw,
+ 0.6,
+ boxstyle="round,pad=0.05",
+ lw=1.5,
+ edgecolor=LN,
+ facecolor=GRAY4,
+ linestyle="--",
+ )
+ ax.add_patch(env_rect)
+ ax.text(
+ 0.3 + bw / 2,
+ 0.6,
+ "SPRZĘT: silniki, czujniki, efektory",
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontstyle="italic",
+ )
+
+ # Arrow from controller to hardware
+ draw_arrow(ax, 2.3, 1.2, 2.3, 0.9, lw=1.3)
+
+ # Abstraction label on the right
+ ax.annotate(
+ "",
+ xy=(6.9, 4.8),
+ xytext=(6.9, 0.5),
+ arrowprops={"arrowstyle": "<->", "color": "#888888", "lw": 1.0},
+ )
+ ax.text(
+ 6.95,
+ 2.65,
+ "abstrakcja",
+ ha="left",
+ va="center",
+ fontsize=7,
+ rotation=90,
+ color="#888888",
+ )
+
+ fig.tight_layout()
+ path = str(Path(OUTPUT_DIR) / "agent_3t_architecture.png")
+ fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" ✓ {path}")
+
+
+# ─── DIAGRAM 3: Behavior Tree Example ────────────────────────────
+def draw_behavior_tree() -> None:
+ """Draw behavior tree."""
+ fig, ax = plt.subplots(1, 1, figsize=(7.5, 4.5), facecolor=BG)
+ ax.set_xlim(0, 7.5)
+ ax.set_ylim(0, 4.5)
+ ax.axis("off")
+ ax.set_title(
+ "Behavior Tree: robot przenoszący obiekt (pick-and-place)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Node positions: (x, y, text, shape_type)
+ # shape_type: 'seq' = sequence (→), 'sel' = selector (?), 'act' = action, 'cond' = condition
+
+ def draw_bt_node(ax, x, y, text, ntype="act", w=1.0, h=0.45) -> tuple[float, float]:
+ """Draw a behavior tree node."""
+ if ntype == "seq":
+ # Sequence = box with →
+ rect = FancyBboxPatch(
+ (x - w / 2, y - h / 2),
+ w,
+ h,
+ boxstyle="round,pad=0.06",
+ lw=1.5,
+ edgecolor=LN,
+ facecolor=GRAY2,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x,
+ y,
+ f"→ {text}",
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ )
+ elif ntype == "sel":
+ # Selector = box with ?
+ rect = FancyBboxPatch(
+ (x - w / 2, y - h / 2),
+ w,
+ h,
+ boxstyle="round,pad=0.06",
+ lw=1.5,
+ edgecolor=LN,
+ facecolor=GRAY3,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x,
+ y,
+ f"? {text}",
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ )
+ elif ntype == "cond":
+ # Condition = diamond-ish / oval with dashed border
+ rect = FancyBboxPatch(
+ (x - w / 2, y - h / 2),
+ w,
+ h,
+ boxstyle="round,pad=0.06",
+ lw=1.0,
+ edgecolor=LN,
+ facecolor="white",
+ linestyle="--",
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x, y, text, ha="center", va="center", fontsize=6.5, fontstyle="italic"
+ )
+ else: # action
+ rect = FancyBboxPatch(
+ (x - w / 2, y - h / 2),
+ w,
+ h,
+ boxstyle="round,pad=0.06",
+ lw=1.0,
+ edgecolor=LN,
+ facecolor=GRAY1,
+ )
+ ax.add_patch(rect)
+ ax.text(x, y, text, ha="center", va="center", fontsize=6.5)
+ return x, y
+
+ # Root: Sequence "Przenieś obiekt"
+ root = draw_bt_node(ax, 3.75, 3.8, "Przenieś obiekt", "seq", w=1.6)
+
+ # Level 2 children
+ find = draw_bt_node(ax, 1.2, 2.8, "Znajdź obiekt", "sel", w=1.3)
+ nav = draw_bt_node(ax, 3.75, 2.8, "Jedź do obiektu", "act", w=1.3)
+ pick = draw_bt_node(ax, 6.3, 2.8, "Chwyć i dostarcz", "seq", w=1.4)
+
+ # Arrows from root
+ draw_arrow(ax, root[0], root[1] - 0.225, find[0], find[1] + 0.225, lw=1.0)
+ draw_arrow(ax, root[0], root[1] - 0.225, nav[0], nav[1] + 0.225, lw=1.0)
+ draw_arrow(ax, root[0], root[1] - 0.225, pick[0], pick[1] + 0.225, lw=1.0)
+
+ # Level 3: children of "Znajdź obiekt" (selector)
+ vis = draw_bt_node(ax, 0.55, 1.7, "Widzę\nobiekt?", "cond", w=0.85, h=0.5)
+ scan = draw_bt_node(ax, 1.85, 1.7, "Skanuj\notoczenie", "act", w=0.85, h=0.5)
+ draw_arrow(ax, find[0], find[1] - 0.225, vis[0], vis[1] + 0.25, lw=0.8)
+ draw_arrow(ax, find[0], find[1] - 0.225, scan[0], scan[1] + 0.25, lw=0.8)
+
+ # Level 3: children of "Chwyć i dostarcz" (sequence)
+ grasp = draw_bt_node(ax, 5.4, 1.7, "Chwyć\nobject", "act", w=0.85, h=0.5)
+ deliver = draw_bt_node(ax, 6.5, 1.7, "Jedź do\ncelu", "act", w=0.85, h=0.5)
+ release = draw_bt_node(ax, 7.2, 1.7, "Puść", "act", w=0.55, h=0.5)
+ draw_arrow(ax, pick[0], pick[1] - 0.225, grasp[0], grasp[1] + 0.25, lw=0.8)
+ draw_arrow(ax, pick[0], pick[1] - 0.225, deliver[0], deliver[1] + 0.25, lw=0.8)
+ draw_arrow(ax, pick[0], pick[1] - 0.225, release[0], release[1] + 0.25, lw=0.8)
+
+ # Legend
+ leg_y = 0.5
+ draw_bt_node(ax, 0.8, leg_y, "→ Sequence", "seq", w=1.1, h=0.35)
+ draw_bt_node(ax, 2.3, leg_y, "? Selector", "sel", w=1.0, h=0.35)
+ draw_bt_node(ax, 3.6, leg_y, "Akcja", "act", w=0.8, h=0.35)
+ draw_bt_node(ax, 4.8, leg_y, "Warunek", "cond", w=0.8, h=0.35)
+
+ ax.text(
+ 0.3, leg_y, "Legenda:", ha="left", va="center", fontsize=6.5, fontweight="bold"
+ )
+
+ # Execution note
+ ax.text(
+ 3.75,
+ 0.05,
+ "Wykonanie: od lewej do prawej. Sequence (→) = wszystkie po kolei. "
+ "Selector (?) = pierwszy sukces.",
+ ha="center",
+ va="center",
+ fontsize=6,
+ fontstyle="italic",
+ color="#555555",
+ )
+
+ fig.tight_layout()
+ path = str(Path(OUTPUT_DIR) / "agent_behavior_tree.png")
+ fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" ✓ {path}")
+
+
+# ─── DIAGRAM 4: BDI Model ────────────────────────────────────────
+def draw_bdi_model() -> None:
+ """Draw bdi model."""
+ fig, ax = plt.subplots(1, 1, figsize=(7, 4), facecolor=BG)
+ ax.set_xlim(0, 7)
+ ax.set_ylim(0, 4)
+ ax.axis("off")
+ ax.set_title(
+ "Model BDI agenta (Beliefs-Desires-Intentions)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ bw = 1.6
+ bh = 1.4
+
+ # BELIEFS box
+ draw_box(ax, 0.3, 1.3, bw, bh, "", fill=GRAY1, fontsize=8, fontweight="bold")
+ ax.text(
+ 0.3 + bw / 2,
+ 1.3 + bh - 0.15,
+ "BELIEFS",
+ ha="center",
+ va="top",
+ fontsize=9,
+ fontweight="bold",
+ )
+ ax.text(
+ 0.3 + bw / 2,
+ 1.3 + bh / 2 - 0.1,
+ "(wiedza o \u015bwiecie)\n\n\u2022 mapa pokoju\n\u2022 pozycja robota\n\u2022 drzwi zamkni\u0119te\n\u2022 bateria: 45%",
+ ha="center",
+ va="center",
+ fontsize=6.5,
+ )
+
+ # DESIRES box
+ draw_box(ax, 2.7, 1.3, bw, bh, "", fill=GRAY2, fontsize=8, fontweight="bold")
+ ax.text(
+ 2.7 + bw / 2,
+ 1.3 + bh - 0.15,
+ "DESIRES",
+ ha="center",
+ va="top",
+ fontsize=9,
+ fontweight="bold",
+ )
+ ax.text(
+ 2.7 + bw / 2,
+ 1.3 + bh / 2 - 0.1,
+ "(cele agenta)\n\n• dostarczyć paczkę\n do pokoju 5\n• naładować baterię\n• unikać kolizji",
+ ha="center",
+ va="center",
+ fontsize=6.5,
+ )
+
+ # INTENTIONS box
+ draw_box(ax, 5.1, 1.3, bw, bh, "", fill=GRAY3, fontsize=8, fontweight="bold")
+ ax.text(
+ 5.1 + bw / 2,
+ 1.3 + bh - 0.15,
+ "INTENTIONS",
+ ha="center",
+ va="top",
+ fontsize=9,
+ fontweight="bold",
+ )
+ ax.text(
+ 5.1 + bw / 2,
+ 1.3 + bh / 2 - 0.1,
+ "(aktualny plan)\n\n→ jedź do drzwi\n bocznych\n→ otwórz drzwi\n→ wjedź do pokoju 5",
+ ha="center",
+ va="center",
+ fontsize=6.5,
+ )
+
+ # Arrows
+ draw_arrow(
+ ax,
+ 0.3 + bw,
+ 1.3 + bh / 2 + 0.15,
+ 2.7,
+ 1.3 + bh / 2 + 0.15,
+ lw=1.3,
+ label="informuje",
+ label_offset=0.08,
+ )
+ draw_arrow(
+ ax,
+ 2.7 + bw,
+ 1.3 + bh / 2 + 0.15,
+ 5.1,
+ 1.3 + bh / 2 + 0.15,
+ lw=1.3,
+ label="filtruje → wybiera",
+ label_offset=0.08,
+ )
+
+ # Feedback from intentions back to beliefs (update)
+ ax.annotate(
+ "",
+ xy=(0.3 + bw / 2, 1.3),
+ xytext=(5.1 + bw / 2, 1.3),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": "#666666",
+ "lw": 1.0,
+ "linestyle": "dashed",
+ "connectionstyle": "arc3,rad=0.3",
+ },
+ )
+ ax.text(
+ 3.5,
+ 0.75,
+ "aktualizacja wiedzy po wykonaniu akcji",
+ ha="center",
+ va="center",
+ fontsize=6,
+ fontstyle="italic",
+ color="#666666",
+ )
+
+ # Sensor input arrow
+ draw_arrow(
+ ax,
+ 0.3 + bw / 2,
+ 3.5,
+ 0.3 + bw / 2,
+ 1.3 + bh,
+ lw=1.3,
+ label="percepcja (sensory)",
+ label_offset=0.05,
+ )
+ ax.text(
+ 0.3 + bw / 2,
+ 3.55,
+ "ŚRODOWISKO",
+ ha="center",
+ va="bottom",
+ fontsize=7,
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.2",
+ "facecolor": GRAY4,
+ "edgecolor": LN,
+ "lw": 0.8,
+ },
+ )
+
+ # Action output arrow
+ draw_arrow(
+ ax,
+ 5.1 + bw / 2,
+ 1.3 + bh,
+ 5.1 + bw / 2,
+ 3.5,
+ lw=1.3,
+ label="akcja (efektory)",
+ label_offset=0.05,
+ )
+ ax.text(
+ 5.1 + bw / 2,
+ 3.55,
+ "EFEKTORY",
+ ha="center",
+ va="bottom",
+ fontsize=7,
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.2",
+ "facecolor": GRAY4,
+ "edgecolor": LN,
+ "lw": 0.8,
+ },
+ )
+
+ fig.tight_layout()
+ path = str(Path(OUTPUT_DIR) / "agent_bdi_model.png")
+ fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" ✓ {path}")
+
+
+# ─── MAIN ────────────────────────────────────────────────────────
+if __name__ == "__main__":
+ print("Generating PYTANIE 15 diagrams...")
+ draw_see_think_act()
+ draw_3t_architecture()
+ draw_behavior_tree()
+ draw_bdi_model()
+ print("Done! All diagrams saved to", OUTPUT_DIR)
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_anki.py b/python_pkg/praca_magisterska_video/generate_images/generate_anki.py
new file mode 100755
index 0000000..3fcd66b
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_anki.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python3
+"""Generate Anki flashcards from exam questions in odpowiedzi/ folder.
+
+Creates a tab-separated file compatible with Anki import.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+import re
+
+
+def extract_question_and_answer(filepath) -> list[dict[str, str]]:
+ """Extract main question and key answer points from a markdown file."""
+ with Path(filepath).open(encoding="utf-8") as f:
+ content = f.read()
+
+ cards = []
+
+ # Extract file number for tagging
+ filename = Path(filepath).name
+ match = re.match(r"(\d+)-(.+)\.md", filename)
+ if match:
+ num = match.group(1)
+ topic = match.group(2).replace("-", "_")
+ else:
+ num = "00"
+ topic = "unknown"
+
+ # Extract main title (usually contains the question)
+ title_match = re.search(r"^# (.+)$", content, re.MULTILINE)
+ title = title_match.group(1) if title_match else "Unknown"
+
+ # Extract the main question from ## Pytanie section
+ question_match = re.search(
+ r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
+ )
+ if question_match:
+ main_question = question_match.group(1).strip()
+ main_question = re.sub(r"\s+", " ", main_question)
+ else:
+ main_question = title
+
+ # Extract subject/przedmiot
+ subject_match = re.search(r"Przedmiot:\s*(\w+)", content)
+ subject = subject_match.group(1) if subject_match else "Ogólne"
+
+ # Create main question card - extract key sections for answer
+ answer_parts = []
+
+ # Look for main answer section
+ main_answer = re.search(
+ r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## |\n---\s*\n## |\Z)",
+ content,
+ re.DOTALL,
+ )
+ if main_answer:
+ answer_text = main_answer.group(1)
+ # Extract key points, definitions, headers
+ headers = re.findall(r"### (.+)", answer_text)
+ for h in headers[:5]: # Limit to first 5 headers
+ answer_parts.append(f"• {h}")
+
+ # Also extract key definitions if present
+ definitions = re.findall(r"\*\*([^*]+)\*\*\s*[--:]\s*([^*\n]+)", content)
+ for term, definition in definitions[:3]:
+ if len(definition) > 20 and len(definition) < 200:
+ answer_parts.append(f"• {term}: {definition.strip()}")
+
+ # If we found answer parts, create main card
+ if answer_parts:
+ answer_html = "
".join(answer_parts[:8]) # Limit answer length
+ cards.append(
+ {
+ "question": main_question,
+ "answer": answer_html,
+ "tags": f"egzamin_magisterski pytanie_{num} {subject} {topic}",
+ }
+ )
+
+ # Extract sub-questions and key concepts as additional cards
+ # Look for ### headers with explanations
+ subsections = re.findall(
+ r"### (\d+\.\s+)?(.+?)\n\n(.+?)(?=\n### |\n## |\n---|\Z)", content, re.DOTALL
+ )
+
+ for _, header, body in subsections:
+ if len(header) < 5 or header.startswith("Przykład"):
+ continue
+
+ # Extract first substantive paragraph or key points
+ body_clean = body.strip()
+
+ # Skip very short or code-only sections
+ if len(body_clean) < 50:
+ continue
+
+ # Extract bullet points or first paragraph
+ bullets = re.findall(r"[-•]\s*\*\*(.+?)\*\*[:\s]*([^\n]+)?", body_clean)
+ if bullets:
+ answer_text = "
".join(
+ [
+ f"• {b[0]}: {b[1].strip()}" if b[1] else f"• {b[0]}"
+ for b in bullets[:5]
+ ]
+ )
+ else:
+ # Get first meaningful paragraph
+ paragraphs = [
+ p.strip()
+ for p in body_clean.split("\n\n")
+ if p.strip() and not p.startswith("```") and not p.startswith("|")
+ ]
+ if paragraphs:
+ first_para = paragraphs[0]
+ # Clean markdown
+ first_para = re.sub(r"\*\*(.+?)\*\*", r"\1", first_para)
+ first_para = re.sub(r"\*(.+?)\*", r"\1", first_para)
+ answer_text = first_para[:400]
+ else:
+ continue
+
+ # Create sub-concept card
+ sub_question = f"Co to jest {header}?" if not header.endswith("?") else header
+ if (
+ "Charakterystyka" in header
+ or "Definicja" in header
+ or "Właściwości" in header
+ ):
+ # These are answer-type headers, reframe
+ parent_topic = title.replace("Pytanie", "").strip(": 0123456789")
+ sub_question = f"{header} - {parent_topic}"
+
+ cards.append(
+ {
+ "question": sub_question,
+ "answer": answer_text,
+ "tags": f"egzamin_magisterski pytanie_{num} {subject} {topic} szczegoly",
+ }
+ )
+
+ # Extract key formulas/definitions as separate cards
+ formulas = re.findall(
+ r"\*\*([A-Za-z\s]+(?:formuła|wzór|twierdzenie|definicja|lemat))\*\*[:\s]*\n?(.+?)(?=\n\n|\n\*\*|\Z)",
+ content,
+ re.IGNORECASE | re.DOTALL,
+ )
+ for formula_name, formula_content in formulas:
+ if len(formula_content) > 20:
+ cards.append(
+ {
+ "question": f"Podaj {formula_name.strip()}",
+ "answer": formula_content.strip()[:300],
+ "tags": f"egzamin_magisterski pytanie_{num} {subject} formuly",
+ }
+ )
+
+ return cards
+
+
+def clean_for_anki(text) -> str:
+ """Clean text for Anki import - escape special characters."""
+ # Replace tabs with spaces
+ text = text.replace("\t", " ")
+ # Convert markdown formatting to HTML
+ text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
+ text = re.sub(r"\*(.+?)\*", r"\1", text)
+ # Handle newlines - convert to
for Anki
+ text = text.replace("\n", "
")
+ # Remove multiple
+ text = re.sub(r"(
)+", "
", text)
+ # Remove leading/trailing
+ text = re.sub(r"^
|
$", "", text)
+ # Escape quotes in a way that works with tab-separated
+ text = text.replace('"', """)
+ return text.strip()
+
+
+def main() -> None:
+ """Main."""
+ odpowiedzi_dir = Path("/home/kuchy/praca_magisterska/pytania/odpowiedzi")
+ output_file = Path(
+ "/home/kuchy/praca_magisterska/pytania/anki_egzamin_magisterski.txt"
+ )
+
+ all_cards = []
+
+ # Process each file
+ for md_file in sorted(odpowiedzi_dir.glob("*.md")):
+ print(f"Processing: {md_file.name}")
+ try:
+ cards = extract_question_and_answer(md_file)
+ all_cards.extend(cards)
+ print(f" -> Extracted {len(cards)} cards")
+ except Exception as e:
+ print(f" -> Error: {e}")
+
+ # Write Anki file with headers
+ with Path(output_file).open("w", encoding="utf-8") as f:
+ # Anki file headers
+ f.write("#separator:tab\n")
+ f.write("#html:true\n")
+ f.write("#columns:Front\tBack\tTags\n")
+ f.write("#deck:Egzamin Magisterski ISY\n")
+ f.write("#notetype:Basic\n")
+ f.write("\n")
+
+ for card in all_cards:
+ front = clean_for_anki(card["question"])
+ back = clean_for_anki(card["answer"])
+ tags = card["tags"]
+ f.write(f"{front}\t{back}\t{tags}\n")
+
+ print(f"\n✅ Created {len(all_cards)} flashcards")
+ print(f"📁 Output: {output_file}")
+ print("\nTo import into Anki:")
+ print("1. Open Anki → File → Import")
+ print("2. Select the .txt file")
+ print("3. Verify 'Allow HTML' is checked")
+ print("4. Click Import")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_anki_final.py b/python_pkg/praca_magisterska_video/generate_images/generate_anki_final.py
new file mode 100755
index 0000000..7435559
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_anki_final.py
@@ -0,0 +1,329 @@
+#!/usr/bin/env python3
+"""Generate comprehensive Anki flashcards from exam questions.
+
+Creates tab-separated file for Anki import with proper HTML formatting.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+import re
+
+
+def clean_text(text) -> str:
+ """Clean and format text for Anki."""
+ if not text:
+ return ""
+
+ # Convert markdown formatting to HTML
+ text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
+ text = re.sub(r"(?\1", text)
+
+ # Handle special characters
+ text = text.replace("\t", " ")
+ text = text.replace('"', """)
+
+ # Clean up whitespace but preserve intentional line breaks
+ text = re.sub(r" +", " ", text)
+ return text.strip()
+
+
+def format_list(items, numbered=False) -> str:
+ """Format a list of items as HTML."""
+ if not items:
+ return ""
+
+ tag = "ol" if numbered else "ul"
+ html = f"<{tag}>"
+ for item in items:
+ cleaned = clean_text(item)
+ if cleaned:
+ html += f"{cleaned}"
+ html += f"{tag}>"
+ return html
+
+
+def extract_from_file(filepath) -> list[dict[str, str]]:
+ """Extract flashcard data from a markdown file."""
+ with Path(filepath).open(encoding="utf-8") as f:
+ content = f.read()
+
+ cards = []
+
+ # Get file metadata
+ filename = Path(filepath).name
+ match = re.match(r"(\d+)-(.+)\.md", filename)
+ num = match.group(1) if match else "00"
+ match.group(2).replace("-", "_") if match else "unknown"
+
+ # Extract subject
+ subj_match = re.search(r"Przedmiot:\s*(\w+)", content)
+ subject = subj_match.group(1) if subj_match else "Ogólne"
+
+ # Base tags
+ base_tags = f"egzamin_magisterski pyt{num} {subject}"
+
+ # =====================================================
+ # CARD TYPE 1: Main Exam Question
+ # =====================================================
+ q_match = re.search(
+ r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
+ )
+ if q_match:
+ main_q = re.sub(r"\s+", " ", q_match.group(1).strip())
+
+ # Extract key topics from main answer
+ answer_match = re.search(
+ r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## [�🎯]|\n---\s*\n## |\Z)",
+ content,
+ re.DOTALL,
+ )
+ if answer_match:
+ answer_section = answer_match.group(1)
+ # Get main headers
+ headers = re.findall(
+ r"^### (?:\d+\.\s*)?(.+)$", answer_section, re.MULTILINE
+ )
+ headers = [h.strip() for h in headers if len(h.strip()) > 3][:6]
+
+ if headers:
+ answer_html = "Kluczowe zagadnienia:" + format_list(headers)
+ cards.append(
+ {
+ "front": clean_text(main_q),
+ "back": answer_html,
+ "tags": f"{base_tags} pytanie_glowne",
+ }
+ )
+
+ # =====================================================
+ # CARD TYPE 2: Subsection Cards (detailed concepts)
+ # =====================================================
+ # Find all ### sections
+ sections = re.findall(
+ r"^### (?:\d+\.\s*)?(.+?)\n((?:(?!^###).)+)", content, re.MULTILINE | re.DOTALL
+ )
+
+ for header, body in sections:
+ header = header.strip()
+ body = body.strip()
+
+ # Skip very short sections or example sections
+ if len(body) < 50 or header.lower().startswith("przykład"):
+ continue
+
+ # Extract key information from body
+ answer_parts = []
+
+ # Look for #### sub-headers
+ subheaders = re.findall(r"^#### (.+)$", body, re.MULTILINE)
+ if subheaders:
+ answer_parts.extend(subheaders[:4])
+
+ # Look for bullet points with bold terms
+ bullets = re.findall(r"[-•]\s*\*\*([^*]+)\*\*[:\s-]*([^\n]+)?", body)
+ for term, desc in bullets[:5]:
+ if desc:
+ answer_parts.append(f"{term}: {desc.strip()}")
+ else:
+ answer_parts.append(f"{term}")
+
+ # If no structured content, get first paragraph
+ if not answer_parts:
+ paras = [
+ p.strip()
+ for p in body.split("\n\n")
+ if p.strip()
+ and not p.strip().startswith("```")
+ and not p.strip().startswith("|")
+ ]
+ if paras:
+ first = paras[0]
+ # Limit length
+ if len(first) > 300:
+ first = first[:300] + "..."
+ answer_parts.append(first)
+
+ if answer_parts:
+ # Determine card type
+ if "Definicja" in header or "Co to" in header:
+ q = f"Co to jest: {header.replace('Definicja', '').strip()}?"
+ elif "Charakterystyka" in header:
+ q = f"Scharakteryzuj: {header.replace('Charakterystyka', '').strip()}"
+ elif header.endswith("?"):
+ q = header
+ else:
+ q = f"Omów: {header}"
+
+ # Format answer
+ if len(answer_parts) > 1:
+ answer_html = format_list(answer_parts)
+ else:
+ answer_html = clean_text(answer_parts[0])
+
+ cards.append(
+ {
+ "front": clean_text(q),
+ "back": answer_html,
+ "tags": f"{base_tags} szczegoly",
+ }
+ )
+
+ # =====================================================
+ # CARD TYPE 3: Algorithms/Formulas
+ # =====================================================
+ algo_patterns = [
+ r"#### Złożoność(?:\s+czasowa)?\s*\n(.+?)(?=\n####|\n###|\Z)",
+ r"Złożoność:\s*\*\*([^*]+)\*\*",
+ ]
+
+ for pattern in algo_patterns:
+ matches = re.findall(pattern, content, re.DOTALL)
+ for match in matches[:2]:
+ if len(match) > 10:
+ # Find context - which algorithm?
+ algo_context = re.search(
+ r"### (\d+\.\s*)?(.+?)(?=\n)", content[: content.find(match)]
+ )
+ if algo_context:
+ algo_name = algo_context.group(2).strip()
+ cards.append(
+ {
+ "front": f"Jaka jest złożoność algorytmu/metody: {algo_name}?",
+ "back": clean_text(match.strip()[:200]),
+ "tags": f"{base_tags} zlozonosc",
+ }
+ )
+ break
+
+ # =====================================================
+ # CARD TYPE 4: Comparisons (when file contains comparisons)
+ # =====================================================
+ compare_match = re.search(
+ r"## .*(Porównanie|Zestawienie|vs).*\n(.+?)(?=\n## |\Z)",
+ content,
+ re.DOTALL | re.IGNORECASE,
+ )
+ if compare_match:
+ compare_section = compare_match.group(2)
+ # Extract comparison items
+ items = re.findall(r"\|\s*\*\*([^|*]+)\*\*\s*\|([^|]+)\|", compare_section)
+ if items:
+ comparison_html = "| Aspekt | Wartość |
"
+ for aspect, value in items[:6]:
+ comparison_html += f"| {clean_text(aspect)} | {clean_text(value)} |
"
+ comparison_html += "
"
+
+ # Get comparison title
+ title_match = re.search(
+ r"## .*(Porównanie|Zestawienie).*?(\w+.*?(?:vs|i|oraz).*?\w+)",
+ compare_match.group(0),
+ re.IGNORECASE,
+ )
+ if title_match:
+ cards.append(
+ {
+ "front": f"Porównaj kluczowe różnice w temacie: pytanie {num}",
+ "back": comparison_html,
+ "tags": f"{base_tags} porownanie",
+ }
+ )
+
+ # =====================================================
+ # CARD TYPE 5: Q&A from practice questions section
+ # =====================================================
+ qa_section = re.search(r"## 🎓 Pytania.*?\n(.+?)(?=\n## |\Z)", content, re.DOTALL)
+ if qa_section:
+ qa_content = qa_section.group(1)
+ # Find Q&A pairs
+ qas = re.findall(
+ r'### Q\d+:?\s*["\']?(.+?)["\']?\s*\n.*?Odpowiedź:\s*\n?(.+?)(?=\n### |\Z)',
+ qa_content,
+ re.DOTALL,
+ )
+ for q, a in qas[:3]:
+ q = re.sub(r"\s+", " ", q.strip())
+ a = a.strip()
+ if len(a) > 30:
+ # Limit answer length
+ a_lines = a.split("\n")
+ a_short = "\n".join(a_lines[:5])
+ if len(a_short) > 400:
+ a_short = a_short[:400] + "..."
+
+ cards.append(
+ {
+ "front": clean_text(q),
+ "back": clean_text(a_short).replace("\n", "
"),
+ "tags": f"{base_tags} egzamin_praktyka",
+ }
+ )
+
+ return cards
+
+
+def main() -> None:
+ """Main."""
+ odpowiedzi_dir = Path("/home/kuchy/praca_magisterska/pytania/odpowiedzi")
+ output_file = Path(
+ "/home/kuchy/praca_magisterska/pytania/anki_egzamin_magisterski.txt"
+ )
+
+ all_cards = []
+
+ for md_file in sorted(odpowiedzi_dir.glob("*.md")):
+ print(f"Processing: {md_file.name}", end=" ")
+ try:
+ cards = extract_from_file(md_file)
+ all_cards.extend(cards)
+ print(f"→ {len(cards)} cards")
+ except Exception as e:
+ print(f"→ ERROR: {e}")
+
+ # Remove potential duplicates (same front)
+ seen = set()
+ unique_cards = []
+ for card in all_cards:
+ if card["front"] not in seen:
+ seen.add(card["front"])
+ unique_cards.append(card)
+
+ # Write output file
+ with Path(output_file).open("w", encoding="utf-8") as f:
+ # Anki headers
+ f.write("#separator:tab\n")
+ f.write("#html:true\n")
+ f.write("#tags column:3\n")
+ f.write("#deck:Egzamin Magisterski ISY\n")
+ f.write("#notetype:Basic\n")
+ f.write("\n")
+
+ for card in unique_cards:
+ # Ensure no tabs in content (would break parsing)
+ front = card["front"].replace("\t", " ")
+ back = card["back"].replace("\t", " ")
+ tags = card["tags"]
+
+ f.write(f"{front}\t{back}\t{tags}\n")
+
+ print(f"\n{'=' * 50}")
+ print(f"✅ Generated {len(unique_cards)} unique flashcards")
+ print(f"📁 Saved to: {output_file}")
+ print(f"{'=' * 50}")
+ print("\n📋 IMPORT INSTRUCTIONS:")
+ print("─" * 40)
+ print("Anki Desktop:")
+ print(" 1. File → Import")
+ print(" 2. Select: anki_egzamin_magisterski.txt")
+ print(" 3. Verify: Fields separated by Tab")
+ print(" 4. Check: Allow HTML in fields")
+ print(" 5. Click Import")
+ print()
+ print("AnkiWeb / AnkiDroid:")
+ print(" 1. First import on Anki Desktop")
+ print(" 2. Click Sync to upload to AnkiWeb")
+ print(" 3. Sync on mobile to download")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_anki_v2.py b/python_pkg/praca_magisterska_video/generate_images/generate_anki_v2.py
new file mode 100755
index 0000000..c659fc2
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_anki_v2.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python3
+"""Generate Anki flashcards from exam questions in odpowiedzi/ folder.
+
+Creates a tab-separated file compatible with Anki import.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+import re
+import traceback
+
+
+def extract_main_question(content, filename) -> str:
+ """Extract the main exam question from the file."""
+ # Extract the main question from ## Pytanie section
+ question_match = re.search(
+ r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
+ )
+ if question_match:
+ main_question = question_match.group(1).strip()
+ return re.sub(r"\s+", " ", main_question)
+
+ # Fallback to title
+ title_match = re.search(r"^# (.+)$", content, re.MULTILINE)
+ return title_match.group(1) if title_match else filename
+
+
+def extract_subject(content) -> str:
+ """Extract the subject code."""
+ subject_match = re.search(r"Przedmiot:\s*(\w+)", content)
+ return subject_match.group(1) if subject_match else "Ogólne"
+
+
+def extract_key_points(content) -> list[str]:
+ """Extract key points from the main answer section."""
+ points = []
+
+ # Look for main answer section
+ main_answer = re.search(
+ r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## [^�]|\n---\s*\n## |\Z)",
+ content,
+ re.DOTALL,
+ )
+ if not main_answer:
+ return points
+
+ answer_text = main_answer.group(1)
+
+ # Extract ### headers as key points
+ headers = re.findall(r"^### (.+)$", answer_text, re.MULTILINE)
+ for h in headers[:6]:
+ # Clean header
+ h = re.sub(r"\d+\.\s*", "", h).strip()
+ if h and len(h) > 3:
+ points.append(h)
+
+ return points
+
+
+def extract_definitions(content) -> list[tuple[str, str]]:
+ """Extract key definitions from the content."""
+ definitions = []
+
+ # Pattern for **Term** - definition or **Term**: definition
+ pattern = r"\*\*([^*\n]+)\*\*\s*[--:]\s*([^*\n]{20,150})"
+ matches = re.findall(pattern, content)
+
+ for term, definition in matches:
+ term = term.strip()
+ definition = definition.strip()
+ # Filter out non-definition patterns
+ if (
+ term
+ and definition
+ and not term.startswith("Przykład")
+ and not term.startswith("Uwaga")
+ ):
+ definitions.append((term, definition))
+
+ return definitions[:5]
+
+
+def clean_html(text) -> str:
+ """Convert markdown to HTML and clean for Anki."""
+ if not text:
+ return ""
+
+ # Replace markdown bold/italic with HTML
+ text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
+ text = re.sub(r"\*(.+?)\*", r"\1", text)
+
+ # Clean up special characters
+ text = text.replace("\t", " ")
+ text = text.replace('"', """)
+
+ # Handle newlines - convert to
+ text = text.replace("\n", " ")
+ text = re.sub(r"\s+", " ", text)
+
+ return text.strip()
+
+
+def process_file(filepath) -> list[dict[str, str]]:
+ """Process a single file and return flashcards."""
+ with Path(filepath).open(encoding="utf-8") as f:
+ content = f.read()
+
+ cards = []
+
+ # Extract metadata
+ filename = Path(filepath).name
+ match = re.match(r"(\d+)-(.+)\.md", filename)
+ if match:
+ num = match.group(1)
+ match.group(2).replace("-", "_")
+ else:
+ num = "00"
+
+ subject = extract_subject(content)
+ main_question = extract_main_question(content, filename)
+
+ # Base tags for this question
+ base_tags = f"egzamin_magisterski pytanie_{num} {subject}"
+
+ # Card 1: Main question with key points
+ key_points = extract_key_points(content)
+ if key_points:
+ answer = (
+ ""
+ + "".join([f"- {clean_html(p)}
" for p in key_points])
+ + "
"
+ )
+ cards.append(
+ {"front": clean_html(main_question), "back": answer, "tags": base_tags}
+ )
+
+ # Card 2+: Key definitions as individual cards
+ definitions = extract_definitions(content)
+ for term, definition in definitions:
+ q = f"Definicja: {term}"
+ a = clean_html(definition)
+ cards.append({"front": q, "back": a, "tags": f"{base_tags} definicje"})
+
+ return cards
+
+
+def main() -> None:
+ """Main."""
+ odpowiedzi_dir = Path("/home/kuchy/praca_magisterska/pytania/odpowiedzi")
+ output_file = Path(
+ "/home/kuchy/praca_magisterska/pytania/anki_egzamin_magisterski.txt"
+ )
+
+ all_cards = []
+
+ # Process each file
+ for md_file in sorted(odpowiedzi_dir.glob("*.md")):
+ print(f"Processing: {md_file.name}")
+ try:
+ cards = process_file(md_file)
+ all_cards.extend(cards)
+ print(f" -> {len(cards)} cards")
+ except Exception as e:
+ print(f" -> Error: {e}")
+ traceback.print_exc()
+
+ # Write Anki-compatible file
+ with Path(output_file).open("w", encoding="utf-8") as f:
+ # File headers for Anki
+ f.write("#separator:tab\n")
+ f.write("#html:true\n")
+ f.write("#tags column:3\n")
+ f.write("#deck:Egzamin Magisterski ISY\n")
+ f.write("#notetype:Basic\n")
+ f.write("\n")
+
+ for card in all_cards:
+ front = card["front"]
+ back = card["back"]
+ tags = card["tags"]
+
+ # Ensure no tabs in content
+ front = front.replace("\t", " ")
+ back = back.replace("\t", " ")
+
+ f.write(f"{front}\t{back}\t{tags}\n")
+
+ print(f"\n✅ Created {len(all_cards)} flashcards")
+ print(f"📁 Output: {output_file}")
+ print("\n=== Import Instructions ===")
+ print("1. Open Anki desktop → File → Import")
+ print("2. Select: anki_egzamin_magisterski.txt")
+ print("3. Set 'Fields separated by: Tab'")
+ print("4. Check 'Allow HTML in fields'")
+ print("5. Map: Field 1 → Front, Field 2 → Back, Field 3 → Tags")
+ print("6. Click Import")
+ print("\nFor AnkiWeb/AnkiDroid: Sync after importing on desktop")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_anki_v3.py b/python_pkg/praca_magisterska_video/generate_images/generate_anki_v3.py
new file mode 100755
index 0000000..148729c
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_anki_v3.py
@@ -0,0 +1,307 @@
+#!/usr/bin/env python3
+"""Generate Anki flashcards with ACTUAL substantive answers, not just headers."""
+
+from __future__ import annotations
+
+from pathlib import Path
+import re
+
+
+def clean_text(text) -> str:
+ """Clean text for Anki."""
+ if not text:
+ return ""
+ text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
+ text = re.sub(r"(?\1", text)
+ text = text.replace("\t", " ")
+ text = text.replace('"', """)
+ text = re.sub(r" +", " ", text)
+ return text.strip()
+
+
+def extract_real_answer(content, section_name) -> str | None:
+ """Extract actual content from a section, not just headers."""
+ # Find the section
+ pattern = rf"### (?:\d+\.\s*)?{re.escape(section_name)}\s*\n((?:(?!^### ).)+)"
+ match = re.search(pattern, content, re.MULTILINE | re.DOTALL)
+ if not match:
+ return None
+
+ body = match.group(1).strip()
+
+ # Extract meaningful content
+ lines = []
+
+ # Get subheaders with their first line of content
+ subheader_pattern = r"#### ([^\n]+)\n([^\n#]+)"
+ for sub_header, first_line in re.findall(subheader_pattern, body):
+ lines.append(f"{sub_header.strip()}: {first_line.strip()}")
+
+ # Get bullet points
+ bullet_pattern = r"[-•]\s*\*\*([^*]+)\*\*[:\s-]*([^\n]*)"
+ for term, desc in re.findall(bullet_pattern, body):
+ if desc.strip():
+ lines.append(f"• {term.strip()}: {desc.strip()}")
+ else:
+ lines.append(f"• {term.strip()}")
+
+ # If no structured content, get paragraphs
+ if not lines:
+ paras = [
+ p.strip()
+ for p in body.split("\n\n")
+ if p.strip() and not p.startswith("```") and not p.startswith("|")
+ ]
+ for p in paras[:2]:
+ if len(p) > 20 and len(p) < 400:
+ lines.append(p)
+
+ return "
".join(lines[:6]) if lines else None
+
+
+def extract_cards(filepath) -> list[dict[str, str]]:
+ """Extract flashcards from a file."""
+ with Path(filepath).open(encoding="utf-8") as f:
+ content = f.read()
+
+ cards = []
+ filename = Path(filepath).name
+ match = re.match(r"(\d+)-(.+)\.md", filename)
+ num = match.group(1) if match else "00"
+
+ subj_match = re.search(r"Przedmiot:\s*(\w+)", content)
+ subject = subj_match.group(1) if subj_match else "Ogólne"
+ base_tags = f"egzamin_magisterski pyt{num} {subject}"
+
+ # Get main question
+ q_match = re.search(
+ r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
+ )
+ main_question = re.sub(r"\s+", " ", q_match.group(1).strip()) if q_match else None
+
+ # ===============================================
+ # MAIN CARD: Question with REAL answer summary
+ # ===============================================
+ if main_question:
+ # Build a real answer from the main sections
+ answer_parts = []
+
+ # For automata question - extract key facts about each automaton
+ if "automat" in main_question.lower() or "maszyn" in main_question.lower():
+ # FA
+ fa_match = re.search(
+ r"Automat Skończony.*?Rozpoznawana klasa języków\s*\n\s*\*\*([^*]+)\*\*",
+ content,
+ re.DOTALL,
+ )
+ if fa_match:
+ answer_parts.append(
+ f"Automat Skończony (FA): {fa_match.group(1).strip()}"
+ )
+
+ # PDA
+ pda_match = re.search(
+ r"Automat ze Stosem.*?Rozpoznawana klasa języków\s*\n\s*\*\*([^*]+)\*\*",
+ content,
+ re.DOTALL,
+ )
+ if pda_match:
+ answer_parts.append(
+ f"Automat ze Stosem (PDA): {pda_match.group(1).strip()}"
+ )
+
+ # TM
+ tm_match = re.search(
+ r"Maszyna Turinga.*?Rozpoznawana klasa języków\s*\n\s*\*\*([^*]+)\*\*",
+ content,
+ re.DOTALL,
+ )
+ if tm_match:
+ answer_parts.append(
+ f"Maszyna Turinga (TM): {tm_match.group(1).strip()}"
+ )
+
+ # Generic extraction if specific didn't work
+ if not answer_parts:
+ # Look for key definitions/summaries
+ key_patterns = [
+ r"#### Definicja\s*\n([^\n#]+)",
+ r"#### Charakterystyka\s*\n([^\n#]+)",
+ r"\*\*Definicja[:\s]*\*\*\s*([^\n]+)",
+ ]
+ for pattern in key_patterns:
+ for match in re.findall(pattern, content)[:3]:
+ if len(match) > 20:
+ answer_parts.append(match.strip())
+
+ # Still nothing? Get first substantive paragraph from main answer
+ if not answer_parts:
+ main_answer = re.search(
+ r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## |\Z)", content, re.DOTALL
+ )
+ if main_answer:
+ # Skip headers, get actual content
+ text = main_answer.group(1)
+ paras = re.findall(r"\n\n([^#\n][^\n]{50,300})", text)
+ answer_parts = paras[:3]
+
+ if answer_parts:
+ answer = "
".join([clean_text(p) for p in answer_parts])
+ cards.append(
+ {
+ "front": clean_text(main_question),
+ "back": answer,
+ "tags": f"{base_tags} pytanie_glowne",
+ }
+ )
+
+ # ===============================================
+ # CONCEPT CARDS: Specific topics with real content
+ # ===============================================
+ # Find all ### sections and extract their actual content
+ sections = re.findall(
+ r"^### (?:\d+\.\s*)?([^\n]+)\n((?:(?!^### ).)*)",
+ content,
+ re.MULTILINE | re.DOTALL,
+ )
+
+ for header, body in sections:
+ header = header.strip()
+ body = body.strip()
+
+ # Skip short sections, mnemonics, examples
+ if (
+ len(body) < 80
+ or "Przykład" in header
+ or "Mnemonic" in header
+ or '"' in header
+ ):
+ continue
+
+ # Extract real content
+ answer_lines = []
+
+ # Get definition if present
+ def_match = re.search(r"#### Definicja[^\n]*\n([^\n#]+(?:\n[^\n#]+)?)", body)
+ if def_match:
+ answer_lines.append(def_match.group(1).strip())
+
+ # Get characterization
+ char_match = re.search(r"#### Charakterystyka\s*\n((?:[-•][^\n]+\n?)+)", body)
+ if char_match:
+ bullets = re.findall(
+ r"[-•]\s*\*\*([^*]+)\*\*[:\s]*([^\n]*)", char_match.group(1)
+ )
+ for term, desc in bullets[:4]:
+ answer_lines.append(
+ f"• {term}: {desc.strip()}" if desc else f"• {term}"
+ )
+
+ # Get bullet points if no structured content yet
+ if not answer_lines:
+ bullets = re.findall(r"[-•]\s*\*\*([^*]+)\*\*[:\s]*([^\n]*)", body)
+ for term, desc in bullets[:5]:
+ answer_lines.append(
+ f"• {term}: {desc.strip()}" if desc else f"• {term}"
+ )
+
+ # Get first paragraph if still nothing
+ if not answer_lines:
+ first_para = re.search(r"^([^#\n\-•|`][^\n]{30,250})", body, re.MULTILINE)
+ if first_para:
+ answer_lines.append(first_para.group(1))
+
+ if answer_lines:
+ question = f"Wyjaśnij: {header}" if not header.endswith("?") else header
+ answer = "
".join([clean_text(l) for l in answer_lines])
+
+ cards.append(
+ {
+ "front": clean_text(question),
+ "back": answer,
+ "tags": f"{base_tags} szczegoly",
+ }
+ )
+
+ # ===============================================
+ # Q&A CARDS: From practice questions section
+ # ===============================================
+ qa_matches = re.findall(
+ r'### Q\d+:\s*["\']?([^"\'?\n]+)\?*["\']?\s*\n.*?Odpowiedź:\s*\n(.+?)(?=\n### |\n## |\Z)',
+ content,
+ re.DOTALL,
+ )
+
+ for question, answer in qa_matches[:5]:
+ question = question.strip()
+ answer = answer.strip()
+
+ # Clean up answer - get first meaningful part
+ answer_lines = answer.split("\n")
+ clean_answer = []
+ for line in answer_lines[:6]:
+ line = line.strip()
+ if line and not line.startswith("```") and not line.startswith("|"):
+ clean_answer.append(line)
+
+ if clean_answer:
+ cards.append(
+ {
+ "front": clean_text(question + "?"),
+ "back": "
".join([clean_text(l) for l in clean_answer]),
+ "tags": f"{base_tags} qa",
+ }
+ )
+
+ return cards
+
+
+def main() -> None:
+ """Main."""
+ odpowiedzi_dir = Path("/home/kuchy/praca_magisterska/pytania/odpowiedzi")
+ output_file = Path(
+ "/home/kuchy/praca_magisterska/pytania/anki_egzamin_magisterski.txt"
+ )
+
+ all_cards = []
+
+ for md_file in sorted(odpowiedzi_dir.glob("*.md")):
+ print(f"Processing: {md_file.name}", end=" ")
+ try:
+ cards = extract_cards(md_file)
+ all_cards.extend(cards)
+ print(f"→ {len(cards)} cards")
+ except Exception as e:
+ print(f"→ ERROR: {e}")
+
+ # Remove duplicates
+ seen = set()
+ unique_cards = []
+ for card in all_cards:
+ key = card["front"][:100]
+ if key not in seen:
+ seen.add(key)
+ unique_cards.append(card)
+
+ # Write file
+ with Path(output_file).open("w", encoding="utf-8") as f:
+ f.write("#separator:Tab\n")
+ f.write("#html:true\n")
+ f.write("#notetype:Basic\n")
+ f.write("#deck:Egzamin Magisterski ISY\n")
+ f.write("#columns:Front\tBack\tTags\n")
+ f.write("#tags column:3\n")
+ f.write("\n")
+
+ for card in unique_cards:
+ front = card["front"].replace("\t", " ")
+ back = card["back"].replace("\t", " ")
+ tags = card["tags"]
+ f.write(f"{front}\t{back}\t{tags}\n")
+
+ print(f"\n✅ Generated {len(unique_cards)} flashcards")
+ print(f"📁 Output: {output_file}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_arch_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_arch_diagrams.py
new file mode 100755
index 0000000..14c5008
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_arch_diagrams.py
@@ -0,0 +1,1036 @@
+#!/usr/bin/env python3
+"""Generate architecture modeling diagrams for PYTANIE 13 (AIS).
+
+ 1. TOGAF ADM cycle
+ 2. 4+1 View Model (Kruchten)
+ 3. C4 Model — 4 zoom levels
+ 4. Zachman Framework grid
+ 5. ArchiMate layers.
+
+All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+import numpy as np
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 9
+FS_TITLE = 14
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+# Light grays for B&W contrast
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+
+
+def draw_arrow(ax, x1, y1, x2, y2, lw=1.3) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": "->", "color": LN, "lw": lw},
+ )
+
+
+def draw_line(ax, x1, y1, x2, y2, lw=1.3, ls="-") -> None:
+ """Draw line."""
+ ax.plot([x1, x2], [y1, y2], color=LN, lw=lw, linestyle=ls)
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.5,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=False,
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y), w, h, boxstyle="round,pad=0.2", lw=lw, edgecolor=LN, facecolor=fill
+ )
+ else:
+ rect = plt.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+# =========================================================================
+# 1. TOGAF ADM Cycle
+# =========================================================================
+def generate_togaf_adm() -> None:
+ """Generate togaf adm."""
+ fig, ax = plt.subplots(figsize=(8.27, 8.27))
+ ax.set_xlim(-12, 12)
+ ax.set_ylim(-12, 12)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG)
+ ax.set_title(
+ "TOGAF ADM (Architecture Development Method)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=15,
+ )
+
+ # Center: Requirements Management
+ c = plt.Circle((0, 0), 2.5, lw=2, edgecolor=LN, facecolor=GRAY2)
+ ax.add_patch(c)
+ ax.text(
+ 0,
+ 0,
+ "Requirements\nManagement",
+ ha="center",
+ va="center",
+ fontsize=8,
+ fontweight="bold",
+ )
+
+ # Phases around the circle
+ phases = [
+ ("Preliminary", 90),
+ ("A: Architecture\nVision", 45),
+ ("B: Business\nArchitecture", 0),
+ ("C: Information\nSystems Arch.", -45),
+ ("D: Technology\nArchitecture", -90),
+ ("E: Opportunities\n& Solutions", -135),
+ ("F: Migration\nPlanning", 180),
+ ("G: Implementation\nGovernance", 135),
+ ]
+ # H: Architecture Change Management between G and Preliminary
+ # Put it at ~112 degrees
+
+ R = 8 # radius of phase placement
+ box_w = 4.5
+ box_h = 2.2
+
+ for i, (label, angle_deg) in enumerate(phases):
+ angle = np.radians(angle_deg)
+ cx = R * np.cos(angle)
+ cy = R * np.sin(angle)
+
+ fill = GRAY1 if i % 2 == 0 else GRAY4
+ rect = FancyBboxPatch(
+ (cx - box_w / 2, cy - box_h / 2),
+ box_w,
+ box_h,
+ boxstyle="round,pad=0.2",
+ lw=1.5,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(cx, cy, label, ha="center", va="center", fontsize=7, fontweight="bold")
+
+ # Arrow from this phase toward center (representing link to Requirements)
+ inner_r = 2.8
+ ix = inner_r * np.cos(angle)
+ iy = inner_r * np.sin(angle)
+ outer_r = R - box_w / 2 - 0.3
+ ox = outer_r * np.cos(angle)
+ oy = outer_r * np.sin(angle)
+ # Dashed line to center
+ draw_line(ax, ix, iy, ox * 0.75, oy * 0.75, lw=0.6, ls="--")
+
+ # Curved arrows between successive phases (outer ring)
+ for i in range(len(phases)):
+ a1 = np.radians(phases[i][1])
+ a2 = np.radians(phases[(i + 1) % len(phases)][1])
+
+ # Midpoint arc arrow
+ mid_angle = (a1 + a2) / 2
+ if phases[i][1] < phases[(i + 1) % len(phases)][1]:
+ mid_angle += np.pi # handle wrap
+ ar = R + 0.3
+ ar * np.cos(mid_angle)
+ ar * np.sin(mid_angle)
+
+ # Simple arrow from phase i endpoint to phase i+1 start
+ # arrow a bit outside the boxes
+ src_angle = a1 - np.radians(18)
+ dst_angle = a2 + np.radians(18)
+ sx = (R + 2.8) * np.cos(src_angle)
+ sy = (R + 2.8) * np.sin(src_angle)
+ dx = (R + 2.8) * np.cos(dst_angle)
+ dy = (R + 2.8) * np.sin(dst_angle)
+
+ ax.annotate(
+ "",
+ xy=(dx, dy),
+ xytext=(sx, sy),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": LN,
+ "lw": 1,
+ "connectionstyle": "arc3,rad=0.3",
+ },
+ )
+
+ # Legend note
+ ax.text(
+ 0,
+ -11.5,
+ "Cykl iteracyjny: ka\u017cda faza mo\u017ce wraca\u0107 do wcze\u015bniejszych.\n"
+ "Requirements Management w centrum \u2014 wp\u0142ywa na ka\u017cd\u0105 faz\u0119.",
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontstyle="italic",
+ )
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "togaf_adm.png"),
+ dpi=DPI,
+ facecolor="white",
+ bbox_inches="tight",
+ )
+ plt.close(fig)
+ print(" OK TOGAF ADM")
+
+
+# =========================================================================
+# 2. 4+1 View Model (Kruchten)
+# =========================================================================
+def generate_4plus1() -> None:
+ """Generate 4plus1."""
+ fig, ax = plt.subplots(figsize=(8.27, 6))
+ ax.set_xlim(0, 100)
+ ax.set_ylim(0, 65)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG)
+ ax.set_title(
+ "4+1 View Model (Kruchten, 1995)", fontsize=FS_TITLE, fontweight="bold", pad=12
+ )
+
+ cx, cy = 50, 32
+ # Center: Scenarios (+1)
+ cw, ch = 18, 8
+ draw_box(
+ ax,
+ cx - cw / 2,
+ cy - ch / 2,
+ cw,
+ ch,
+ "+1\nScenarios\n(Use Cases)",
+ fill=GRAY2,
+ lw=2,
+ fontsize=9,
+ fontweight="bold",
+ rounded=True,
+ )
+
+ # 4 views around
+ views = [
+ (
+ "Logical View\n(Funkcjonalno\u015b\u0107:\nklasy, modu\u0142y)",
+ cx,
+ cy + 18,
+ "Programista",
+ ),
+ (
+ "Process View\n(Wsp\u00f3\u0142bie\u017cno\u015b\u0107,\nprzep\u0142yw danych)",
+ cx + 28,
+ cy,
+ "Integrator",
+ ),
+ (
+ "Development View\n(Organizacja kodu:\npakiety, warstwy)",
+ cx,
+ cy - 18,
+ "Developer",
+ ),
+ (
+ "Physical View\n(Wdro\u017cenie:\nserwery, kontenery)",
+ cx - 28,
+ cy,
+ "Admin/DevOps",
+ ),
+ ]
+
+ bw, bh = 22, 10
+ for label, vx, vy, stakeholder in views:
+ draw_box(
+ ax,
+ vx - bw / 2,
+ vy - bh / 2,
+ bw,
+ bh,
+ label,
+ fill=GRAY1,
+ lw=1.5,
+ fontsize=8,
+ fontweight="bold",
+ rounded=True,
+ )
+ # Label stakeholder below/beside
+ if vy > cy:
+ ax.text(
+ vx,
+ vy + bh / 2 + 1.5,
+ f"\u2190 {stakeholder}",
+ fontsize=7,
+ ha="center",
+ fontstyle="italic",
+ )
+ elif vy < cy:
+ ax.text(
+ vx,
+ vy - bh / 2 - 1.5,
+ f"\u2190 {stakeholder}",
+ fontsize=7,
+ ha="center",
+ fontstyle="italic",
+ )
+ elif vx > cx:
+ ax.text(
+ vx + bw / 2 + 1,
+ vy,
+ f"\u2190 {stakeholder}",
+ fontsize=7,
+ va="center",
+ fontstyle="italic",
+ )
+ else:
+ ax.text(
+ vx - bw / 2 - 1,
+ vy,
+ f"{stakeholder} \u2192",
+ fontsize=7,
+ va="center",
+ ha="right",
+ fontstyle="italic",
+ )
+
+ # Arrow from view to center
+ # Calculate direction
+ dx = cx - vx
+ dy = cy - vy
+ dist = np.sqrt(dx**2 + dy**2)
+ ndx, ndy = dx / dist, dy / dist
+ # Start from edge of view box, end at edge of center box
+ sx = vx + ndx * (bw / 2 + 0.5) if abs(dx) > abs(dy) else vx + ndx * 2
+ sy = vy + ndy * (bh / 2 + 0.5) if abs(dy) > abs(dx) else vy + ndy * 2
+ ex = cx - ndx * (cw / 2 + 0.5) if abs(dx) > abs(dy) else cx - ndx * 2
+ ey = cy - ndy * (ch / 2 + 0.5) if abs(dy) > abs(dx) else cy - ndy * 2
+
+ draw_arrow(ax, sx, sy, ex, ey, lw=1.2)
+
+ # Note
+ ax.text(
+ 50,
+ 2,
+ "Ka\u017cdy widok odpowiada innemu interesariuszowi.\n"
+ "Scenarios (\u0142\u0105cz\u0105cy +1) weryfikuj\u0105 sp\u00f3jno\u015b\u0107 4 widok\u00f3w.",
+ ha="center",
+ fontsize=7,
+ fontstyle="italic",
+ )
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "4plus1_view_model.png"),
+ dpi=DPI,
+ facecolor="white",
+ bbox_inches="tight",
+ )
+ plt.close(fig)
+ print(" OK 4+1 View Model")
+
+
+# =========================================================================
+# 3. C4 Model — 4 Zoom Levels
+# =========================================================================
+def generate_c4() -> None:
+ """Generate c4."""
+ fig, axes = plt.subplots(2, 2, figsize=(8.27, 10))
+ fig.patch.set_facecolor(BG)
+ fig.suptitle(
+ "C4 Model (Simon Brown) \u2014 4 poziomy zoomu",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=0.98,
+ )
+
+ titles = [
+ "Level 1: System Context",
+ "Level 2: Container",
+ "Level 3: Component",
+ "Level 4: Code (UML)",
+ ]
+
+ for idx, ax_item in enumerate(axes.flat):
+ ax_item.set_xlim(0, 100)
+ ax_item.set_ylim(0, 80)
+ ax_item.set_aspect("equal")
+ ax_item.axis("off")
+ ax_item.set_title(titles[idx], fontsize=10, fontweight="bold", pad=8)
+
+ # --- Level 1: System Context ---
+ ax1 = axes[0, 0]
+ # Person
+ ax1.add_patch(plt.Circle((20, 55), 4, lw=1.5, edgecolor=LN, facecolor=GRAY1))
+ # Head (smaller circle on top)
+ ax1.add_patch(plt.Circle((20, 57.5), 1.5, lw=1.2, edgecolor=LN, facecolor="white"))
+ # Body (lines)
+ draw_line(ax1, 20, 56, 20, 52.5, lw=1.2)
+ draw_line(ax1, 17, 55, 23, 55, lw=1.2)
+ ax1.text(20, 48, "Klient", ha="center", fontsize=8, fontweight="bold")
+
+ # System
+ draw_box(
+ ax1,
+ 38,
+ 43,
+ 24,
+ 18,
+ "System\nE-commerce",
+ fill=GRAY2,
+ lw=2,
+ fontsize=9,
+ fontweight="bold",
+ rounded=True,
+ )
+
+ # External
+ draw_box(
+ ax1,
+ 72,
+ 48,
+ 20,
+ 12,
+ "System\nP\u0142atno\u015bci\n(zewn.)",
+ fill=GRAY4,
+ lw=1.5,
+ fontsize=7,
+ rounded=True,
+ )
+ ax1.add_patch(
+ plt.Rectangle(
+ (72, 48), 20, 12, lw=1.5, edgecolor=LN, facecolor="none", linestyle="--"
+ )
+ )
+
+ draw_arrow(ax1, 24, 54, 38, 54)
+ ax1.text(31, 56, "sk\u0142ada\nzam\u00f3wienia", fontsize=6, ha="center")
+ draw_arrow(ax1, 62, 54, 72, 54)
+ ax1.text(67, 56, "API", fontsize=6, ha="center")
+
+ ax1.text(
+ 50,
+ 20,
+ "Kto u\u017cywa systemu?\nZ czym si\u0119 integruje?",
+ ha="center",
+ fontsize=7,
+ fontstyle="italic",
+ bbox={"boxstyle": "round", "facecolor": GRAY4, "edgecolor": LN, "lw": 0.5},
+ )
+
+ # --- Level 2: Container ---
+ ax2 = axes[0, 1]
+ # Big system boundary
+ ax2.add_patch(
+ plt.Rectangle(
+ (5, 15), 90, 58, lw=1.5, edgecolor=LN, facecolor="none", linestyle="--"
+ )
+ )
+ ax2.text(
+ 50,
+ 75,
+ "System E-commerce",
+ ha="center",
+ fontsize=8,
+ fontweight="bold",
+ fontstyle="italic",
+ )
+
+ containers = [
+ ("SPA\n(React)", 15, 50, 18, 12, GRAY1),
+ ("API\nServer\n(Node.js)", 42, 50, 18, 12, GRAY2),
+ ("Database\n(PostgreSQL)", 70, 50, 18, 12, GRAY3),
+ ("Worker\n(Python)", 42, 25, 18, 12, GRAY1),
+ ]
+ for label, x, y, w, h, fill in containers:
+ draw_box(
+ ax2,
+ x,
+ y,
+ w,
+ h,
+ label,
+ fill=fill,
+ lw=1.5,
+ fontsize=7,
+ fontweight="bold",
+ rounded=True,
+ )
+
+ draw_arrow(ax2, 33, 56, 42, 56)
+ ax2.text(37.5, 58, "REST", fontsize=6, ha="center")
+ draw_arrow(ax2, 60, 56, 70, 56)
+ ax2.text(65, 58, "SQL", fontsize=6, ha="center")
+ draw_arrow(ax2, 51, 50, 51, 37)
+ ax2.text(53, 44, "async", fontsize=6)
+
+ ax2.text(
+ 50,
+ 8,
+ "Jakie kontenery techniczne\nsk\u0142adaj\u0105 si\u0119 na system?",
+ ha="center",
+ fontsize=7,
+ fontstyle="italic",
+ bbox={"boxstyle": "round", "facecolor": GRAY4, "edgecolor": LN, "lw": 0.5},
+ )
+
+ # --- Level 3: Component ---
+ ax3 = axes[1, 0]
+ ax3.add_patch(
+ plt.Rectangle(
+ (5, 15), 90, 58, lw=1.5, edgecolor=LN, facecolor="none", linestyle="--"
+ )
+ )
+ ax3.text(
+ 50,
+ 75,
+ "API Server (Node.js)",
+ ha="center",
+ fontsize=8,
+ fontweight="bold",
+ fontstyle="italic",
+ )
+
+ components = [
+ ("OrderController", 10, 50, 22, 10, GRAY1),
+ ("AuthService", 40, 50, 22, 10, GRAY2),
+ ("PaymentGateway\n(adapter)", 70, 50, 22, 10, GRAY1),
+ ("OrderRepository", 25, 25, 22, 10, GRAY2),
+ ("NotificationService", 57, 25, 22, 10, GRAY1),
+ ]
+ for label, x, y, w, h, fill in components:
+ draw_box(
+ ax3,
+ x,
+ y,
+ w,
+ h,
+ label,
+ fill=fill,
+ lw=1.5,
+ fontsize=6.5,
+ fontweight="bold",
+ rounded=True,
+ )
+
+ draw_arrow(ax3, 32, 55, 40, 55)
+ draw_arrow(ax3, 62, 55, 70, 55)
+ draw_arrow(ax3, 21, 50, 30, 35)
+ draw_arrow(ax3, 51, 50, 62, 35)
+
+ ax3.text(
+ 50,
+ 8,
+ "Jakie modu\u0142y/komponenty\nwewn\u0105trz kontenera?",
+ ha="center",
+ fontsize=7,
+ fontstyle="italic",
+ bbox={"boxstyle": "round", "facecolor": GRAY4, "edgecolor": LN, "lw": 0.5},
+ )
+
+ # --- Level 4: Code ---
+ ax4 = axes[1, 1]
+
+ # UML-style class boxes
+ def draw_class(ax, x, y, name, attrs, methods, w=28, fill=GRAY1) -> None:
+ """Draw class."""
+ h_name = 6
+ h_attr = len(attrs) * 4 + 2
+ h_meth = len(methods) * 4 + 2
+ h_total = h_name + h_attr + h_meth
+ # Name
+ ax.add_patch(
+ plt.Rectangle((x, y), w, h_total, lw=1.5, edgecolor=LN, facecolor=fill)
+ )
+ ax.plot(
+ [x, x + w], [y + h_total - h_name, y + h_total - h_name], color=LN, lw=1
+ )
+ ax.text(
+ x + w / 2,
+ y + h_total - h_name / 2,
+ name,
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ )
+ # Attrs
+ ax.plot([x, x + w], [y + h_meth, y + h_meth], color=LN, lw=1)
+ for i, a in enumerate(attrs):
+ ax.text(
+ x + 2,
+ y + h_total - h_name - 2 - i * 4,
+ a,
+ fontsize=6,
+ va="top",
+ family="monospace",
+ )
+ # Methods
+ for i, m in enumerate(methods):
+ ax.text(
+ x + 2,
+ y + h_meth - 2 - i * 4,
+ m,
+ fontsize=6,
+ va="top",
+ family="monospace",
+ )
+
+ draw_class(
+ ax4,
+ 5,
+ 40,
+ "\u00abinterface\u00bb\nIOrderRepository",
+ [],
+ ["+save(order)", "+findById(id)"],
+ w=32,
+ fill=GRAY4,
+ )
+ draw_class(
+ ax4,
+ 55,
+ 40,
+ "OrderRepository",
+ ["-db: Database"],
+ ["+save(order)", "+findById(id)"],
+ w=32,
+ fill=GRAY1,
+ )
+ draw_class(
+ ax4,
+ 30,
+ 10,
+ "Order",
+ ["-id: UUID", "-items: List", "-total: Money"],
+ ["+addItem(item)", "+calculateTotal()"],
+ w=32,
+ fill=GRAY2,
+ )
+
+ # Implements arrow (dashed)
+ ax4.annotate(
+ "",
+ xy=(37, 46),
+ xytext=(55, 50),
+ arrowprops={"arrowstyle": "-|>", "color": LN, "lw": 1.2, "linestyle": "--"},
+ )
+ ax4.text(
+ 46, 52, "\u00abimplements\u00bb", fontsize=6, ha="center", fontstyle="italic"
+ )
+
+ # Dependency
+ draw_arrow(ax4, 71, 40, 50, 24)
+ ax4.text(64, 32, "uses", fontsize=6, fontstyle="italic")
+
+ ax4.text(
+ 50,
+ 3,
+ "Diagramy klas UML\n(opcjonalny poziom szczeg\u00f3\u0142owo\u015bci)",
+ ha="center",
+ fontsize=7,
+ fontstyle="italic",
+ bbox={"boxstyle": "round", "facecolor": GRAY4, "edgecolor": LN, "lw": 0.5},
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.96])
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "c4_model.png"),
+ dpi=DPI,
+ facecolor="white",
+ bbox_inches="tight",
+ )
+ plt.close(fig)
+ print(" OK C4 Model")
+
+
+# =========================================================================
+# 4. Zachman Framework Grid
+# =========================================================================
+def generate_zachman() -> None:
+ """Generate zachman."""
+ fig, ax = plt.subplots(figsize=(8.27, 6))
+ ax.set_xlim(0, 100)
+ ax.set_ylim(0, 65)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG)
+ ax.set_title(
+ "Zachman Framework \u2014 taksonomia architektury",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ rows = [
+ "Kontekst\n(Planner)",
+ "Konceptualny\n(Owner)",
+ "Logiczny\n(Designer)",
+ "Fizyczny\n(Builder)",
+ "Szczeg\u00f3\u0142owy\n(Subcontractor)",
+ ]
+ cols = [
+ "Co?\n(dane)",
+ "Jak?\n(funkcje)",
+ "Gdzie?\n(sie\u0107)",
+ "Kto?\n(ludzie)",
+ "Kiedy?\n(czas)",
+ "Dlaczego?\n(cel)",
+ ]
+
+ n_rows = len(rows)
+ n_cols = len(cols)
+
+ x0 = 18
+ y0 = 5
+ cw = 12.5 # cell width
+ ch = 9 # cell height
+ rh_label = 14 # row label width
+
+ # Column headers
+ for j, col in enumerate(cols):
+ x = x0 + j * cw
+ draw_box(
+ ax,
+ x,
+ y0 + n_rows * ch,
+ cw,
+ 7,
+ col,
+ fill=GRAY2,
+ lw=1.5,
+ fontsize=6.5,
+ fontweight="bold",
+ )
+
+ # Row headers
+ for i, row in enumerate(rows):
+ y = y0 + (n_rows - 1 - i) * ch
+ draw_box(
+ ax,
+ x0 - rh_label,
+ y,
+ rh_label,
+ ch,
+ row,
+ fill=GRAY2,
+ lw=1.5,
+ fontsize=6.5,
+ fontweight="bold",
+ )
+
+ # Cells
+ fills = [GRAY4, "white"]
+ for i in range(n_rows):
+ for j in range(n_cols):
+ x = x0 + j * cw
+ y = y0 + (n_rows - 1 - i) * ch
+ fill = fills[(i + j) % 2]
+ ax.add_patch(
+ plt.Rectangle((x, y), cw, ch, lw=0.8, edgecolor=LN, facecolor=fill)
+ )
+
+ # Sample content in a few cells
+ examples = {
+ (0, 0): "Lista\nencji",
+ (0, 1): "Lista\nproces\u00f3w",
+ (0, 2): "Lokalizacje",
+ (1, 0): "Model\npoj\u0119ciowy",
+ (1, 1): "Model\nproces\u00f3w",
+ (2, 0): "ERD",
+ (2, 1): "Data Flow",
+ (3, 0): "Schemat\nDB",
+ (3, 1): "Kod\nprogramu",
+ (0, 3): "Role",
+ (1, 3): "Org chart",
+ (0, 4): "Harmonogram",
+ (0, 5): "Cele\nbiznesowe",
+ }
+ for (i, j), text in examples.items():
+ x = x0 + j * cw
+ y = y0 + (n_rows - 1 - i) * ch
+ ax.text(
+ x + cw / 2,
+ y + ch / 2,
+ text,
+ ha="center",
+ va="center",
+ fontsize=5.5,
+ fontstyle="italic",
+ color="#444444",
+ )
+
+ # Note
+ ax.text(
+ 50,
+ 1,
+ "Ka\u017cda kom\u00f3rka = artefakt opisuj\u0105cy system z danej perspektywy i aspektu.\n"
+ "Zachman nie m\u00f3wi JAK modelowa\u0107 \u2014 m\u00f3wi CO nale\u017cy udokumentowa\u0107.",
+ ha="center",
+ fontsize=7,
+ fontstyle="italic",
+ )
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "zachman_framework.png"),
+ dpi=DPI,
+ facecolor="white",
+ bbox_inches="tight",
+ )
+ plt.close(fig)
+ print(" OK Zachman Framework")
+
+
+# =========================================================================
+# 5. ArchiMate Layers
+# =========================================================================
+def generate_archimate() -> None:
+ """Generate archimate."""
+ fig, ax = plt.subplots(figsize=(8.27, 9))
+ ax.set_xlim(0, 100)
+ ax.set_ylim(0, 100)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG)
+ ax.set_title(
+ "ArchiMate \u2014 3 warstwy \u00d7 3 aspekty",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Column headers (aspects)
+ headers = [
+ ("Active Structure\n(KTO?)", 0),
+ ("Behavior\n(CO robi?)", 1),
+ ("Passive Structure\n(NA CZYM?)", 2),
+ ]
+
+ x0 = 10
+ y0 = 10
+ cw = 26
+ ch = 20
+ gap = 1
+ header_h = 8
+ row_label_w = 14
+
+ # Column headers
+ for label, j in headers:
+ x = x0 + row_label_w + j * (cw + gap)
+ draw_box(
+ ax,
+ x,
+ y0 + 3 * (ch + gap),
+ cw,
+ header_h,
+ label,
+ fill=GRAY3,
+ lw=1.5,
+ fontsize=8,
+ fontweight="bold",
+ )
+
+ # Layers (rows)
+ layers = [
+ (
+ "Business\nLayer",
+ GRAY1,
+ [
+ ("Business\nActor", "Business\nProcess", "Business\nObject"),
+ ("(Kto wykonuje?)", "(Co si\u0119 dzieje?)", "(Na czym dzia\u0142a?)"),
+ (
+ "np. Klient,\nHandlowiec",
+ "np. Obs\u0142uga\nzam\u00f3wienia",
+ "np. Zam\u00f3wienie,\nFaktura",
+ ),
+ ],
+ ),
+ (
+ "Application\nLayer",
+ GRAY4,
+ [
+ ("Application\nComponent", "Application\nService", "Data\nObject"),
+ ("(Jaki modu\u0142?)", "(Jaka us\u0142uga?)", "(Jakie dane?)"),
+ ("np. CRM,\nERP", "np. API\nzam\u00f3wie\u0144", "np. tabela\nOrders"),
+ ],
+ ),
+ (
+ "Technology\nLayer",
+ "white",
+ [
+ ("Node /\nDevice", "Infrastructure\nService", "Artifact"),
+ ("(Jaki sprz\u0119t?)", "(Jaka infra?)", "(Jaki plik?)"),
+ (
+ "np. Serwer\nLinux, K8s",
+ "np. Load\nBalancer",
+ "np. .jar,\n.war, image",
+ ),
+ ],
+ ),
+ ]
+
+ for i, (layer_name, fill, cells) in enumerate(layers):
+ y = y0 + (2 - i) * (ch + gap)
+
+ # Row label
+ draw_box(
+ ax,
+ x0,
+ y,
+ row_label_w,
+ ch,
+ layer_name,
+ fill=GRAY2,
+ lw=1.5,
+ fontsize=8,
+ fontweight="bold",
+ )
+
+ for j in range(3):
+ x = x0 + row_label_w + j * (cw + gap)
+ ax.add_patch(
+ plt.Rectangle((x, y), cw, ch, lw=1.5, edgecolor=LN, facecolor=fill)
+ )
+ # Element name (bold)
+ ax.text(
+ x + cw / 2,
+ y + ch - 3,
+ cells[0][j],
+ ha="center",
+ va="top",
+ fontsize=7,
+ fontweight="bold",
+ )
+ # Role description
+ ax.text(
+ x + cw / 2,
+ y + ch / 2,
+ cells[1][j],
+ ha="center",
+ va="center",
+ fontsize=6,
+ fontstyle="italic",
+ color="#555555",
+ )
+ # Example
+ ax.text(
+ x + cw / 2,
+ y + 3,
+ cells[2][j],
+ ha="center",
+ va="bottom",
+ fontsize=6,
+ color="#333333",
+ )
+
+ # Vertical arrows between layers
+ for j in range(3):
+ x = x0 + row_label_w + j * (cw + gap) + cw / 2
+ for i in range(2):
+ y_top = y0 + (2 - i) * (ch + gap)
+ y_bot = y0 + (2 - i - 1) * (ch + gap) + ch
+ draw_arrow(ax, x, y_top, x, y_bot + 0.3, lw=1)
+
+ # Arrow labels
+ mid_x = x0 + row_label_w - 3
+ ax.text(
+ mid_x,
+ y0 + 2 * (ch + gap) - gap / 2,
+ "realizacja \u2193",
+ fontsize=6,
+ ha="right",
+ va="center",
+ fontstyle="italic",
+ rotation=90,
+ )
+ ax.text(
+ mid_x,
+ y0 + 1 * (ch + gap) - gap / 2,
+ "realizacja \u2193",
+ fontsize=6,
+ ha="right",
+ va="center",
+ fontstyle="italic",
+ rotation=90,
+ )
+
+ # Note
+ ax.text(
+ 50,
+ 4,
+ "Warstwy czytamy z g\u00f3ry (biznes) na d\u00f3\u0142 (technologia).\n"
+ "Ni\u017csze warstwy REALIZUJ\u0104 wy\u017csze. "
+ "ArchiMate jest komplementarny z TOGAF.",
+ ha="center",
+ fontsize=7,
+ fontstyle="italic",
+ )
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "archimate_layers.png"),
+ dpi=DPI,
+ facecolor="white",
+ bbox_inches="tight",
+ )
+ plt.close(fig)
+ print(" OK ArchiMate")
+
+
+# =========================================================================
+if __name__ == "__main__":
+ print(f"Generating architecture diagrams to {OUTPUT_DIR}/...")
+ generate_togaf_adm()
+ generate_4plus1()
+ generate_c4()
+ generate_zachman()
+ generate_archimate()
+ print(f"\nAll diagrams saved to {OUTPUT_DIR}/")
+ for f in sorted([p.name for p in Path(OUTPUT_DIR).iterdir()]):
+ if (
+ "togaf" in f
+ or "4plus1" in f
+ or "c4" in f
+ or "zachman" in f
+ or "archimate" in f
+ ):
+ size_kb = Path(str(Path(OUTPUT_DIR).stat().st_size / f)) / 1024
+ print(f" {f} ({size_kb:.0f} KB)")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_automata_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_automata_diagrams.py
new file mode 100755
index 0000000..c07f7ba
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_automata_diagrams.py
@@ -0,0 +1,887 @@
+#!/usr/bin/env python3
+"""Generate diagrams for PYTANIE 1: Automata and language classes.
+
+ 1. FA recognition example — DFA for strings ending in "ab"
+ 2. PDA recognition example — PDA for aⁿbⁿ
+ 3. LBA recognition example — LBA for aⁿbⁿcⁿ
+ 4. TM recognition example — TM for 0ⁿ1ⁿ.
+
+All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+import matplotlib.pyplot as plt
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 11
+FS_SMALL = 6.5
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+LIGHT_GREEN = "#D5E8D4"
+LIGHT_RED = "#F8D7DA"
+LIGHT_BLUE = "#D6EAF8"
+LIGHT_YELLOW = "#FFF9C4"
+
+
+def draw_state_circle(
+ ax, x, y, r, label, accepting=False, initial=False, fillcolor="white", fontsize=FS
+) -> None:
+ """Draw an automaton state circle."""
+ circle = plt.Circle(
+ (x, y), r, fill=True, facecolor=fillcolor, edgecolor=LN, linewidth=1.5, zorder=3
+ )
+ ax.add_patch(circle)
+ if accepting:
+ inner = plt.Circle(
+ (x, y), r * 0.82, fill=False, edgecolor=LN, linewidth=1.2, zorder=3
+ )
+ ax.add_patch(inner)
+ if initial:
+ ax.annotate(
+ "",
+ xy=(x - r, y),
+ xytext=(x - r - 0.4, y),
+ arrowprops={"arrowstyle": "->", "color": LN, "lw": 1.5},
+ zorder=4,
+ )
+ ax.text(
+ x,
+ y,
+ label,
+ ha="center",
+ va="center",
+ fontsize=fontsize,
+ fontweight="bold",
+ zorder=5,
+ )
+
+
+def draw_curved_arrow(
+ ax,
+ x1,
+ y1,
+ x2,
+ y2,
+ label,
+ _r=0.25,
+ connectionstyle="arc3,rad=0.3",
+ fontsize=FS_SMALL,
+ label_offset=(0, 0),
+) -> None:
+ """Draw a curved arrow between points with label."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": LN,
+ "lw": 1.2,
+ "connectionstyle": connectionstyle,
+ },
+ zorder=2,
+ )
+ mx = (x1 + x2) / 2 + label_offset[0]
+ my = (y1 + y2) / 2 + label_offset[1]
+ ax.text(
+ mx,
+ my,
+ label,
+ ha="center",
+ va="center",
+ fontsize=fontsize,
+ fontstyle="italic",
+ zorder=5,
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "white",
+ "edgecolor": "none",
+ "alpha": 0.9,
+ },
+ )
+
+
+def draw_self_loop(ax, x, y, r, label, direction="top", fontsize=FS_SMALL) -> None:
+ """Draw a self-loop on a state."""
+ if direction == "top":
+ loop = mpatches.FancyArrowPatch(
+ (x - 0.12, y + r),
+ (x + 0.12, y + r),
+ connectionstyle="arc3,rad=-1.8",
+ arrowstyle="->",
+ mutation_scale=12,
+ lw=1.2,
+ color=LN,
+ zorder=2,
+ )
+ ax.add_patch(loop)
+ ax.text(
+ x,
+ y + r + 0.35,
+ label,
+ ha="center",
+ va="center",
+ fontsize=fontsize,
+ fontstyle="italic",
+ zorder=5,
+ )
+ elif direction == "bottom":
+ loop = mpatches.FancyArrowPatch(
+ (x - 0.12, y - r),
+ (x + 0.12, y - r),
+ connectionstyle="arc3,rad=1.8",
+ arrowstyle="->",
+ mutation_scale=12,
+ lw=1.2,
+ color=LN,
+ zorder=2,
+ )
+ ax.add_patch(loop)
+ ax.text(
+ x,
+ y - r - 0.35,
+ label,
+ ha="center",
+ va="center",
+ fontsize=fontsize,
+ fontstyle="italic",
+ zorder=5,
+ )
+
+
+# ============================================================
+# 1. FA Recognition Example — DFA for strings ending in "ab"
+# ============================================================
+def draw_fa_recognition() -> None:
+ """FA state diagram + step-by-step trace for 'baab'."""
+ _fig, axes = plt.subplots(
+ 1, 2, figsize=(11.69, 4), gridspec_kw={"width_ratios": [1, 1.3]}
+ )
+
+ # --- Left: State diagram ---
+ ax = axes[0]
+ ax.set_xlim(-1, 5.5)
+ ax.set_ylim(-1.5, 2.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ 'DFA — diagram stanów\nL = {słowa nad {a,b} kończące się na "ab"}',
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ R = 0.35
+ # States positions
+ states = {"q₀": (0.8, 0.5), "q₁": (2.8, 0.5), "q₂": (4.8, 0.5)}
+
+ draw_state_circle(ax, *states["q₀"], R, "q₀", initial=True)
+ draw_state_circle(ax, *states["q₁"], R, "q₁")
+ draw_state_circle(ax, *states["q₂"], R, "q₂", accepting=True, fillcolor=LIGHT_GREEN)
+
+ # Transitions
+ # q₀ --a--> q₁
+ draw_curved_arrow(
+ ax,
+ states["q₀"][0] + R,
+ states["q₀"][1] + 0.05,
+ states["q₁"][0] - R,
+ states["q₁"][1] + 0.05,
+ "a",
+ connectionstyle="arc3,rad=0.15",
+ label_offset=(0, 0.25),
+ )
+ # q₁ --b--> q₂
+ draw_curved_arrow(
+ ax,
+ states["q₁"][0] + R,
+ states["q₁"][1] + 0.05,
+ states["q₂"][0] - R,
+ states["q₂"][1] + 0.05,
+ "b",
+ connectionstyle="arc3,rad=0.15",
+ label_offset=(0, 0.25),
+ )
+ # q₂ --a--> q₁
+ draw_curved_arrow(
+ ax,
+ states["q₂"][0] - R,
+ states["q₂"][1] - 0.05,
+ states["q₁"][0] + R,
+ states["q₁"][1] - 0.05,
+ "a",
+ connectionstyle="arc3,rad=0.15",
+ label_offset=(0, -0.3),
+ )
+ # q₂ --b--> q₀
+ draw_curved_arrow(
+ ax,
+ states["q₂"][0] - 0.2,
+ states["q₂"][1] - R,
+ states["q₀"][0] + 0.2,
+ states["q₀"][1] - R,
+ "b",
+ connectionstyle="arc3,rad=0.4",
+ label_offset=(0, -0.4),
+ )
+ # q₀ --b--> q₀ (self-loop)
+ draw_self_loop(ax, *states["q₀"], R, "b", direction="top")
+ # q₁ --a--> q₁ (self-loop)
+ draw_self_loop(ax, *states["q₁"], R, "a", direction="top")
+
+ # Legend
+ ax.text(
+ 0.3,
+ -1.0,
+ "→ = start ◎ = akceptujący",
+ fontsize=FS_SMALL,
+ ha="left",
+ va="center",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # --- Right: Step-by-step trace ---
+ ax2 = axes[1]
+ ax2.axis("off")
+ ax2.set_title(
+ 'Ślad wykonania — wejście: "baab"', fontsize=FS_TITLE, fontweight="bold", pad=10
+ )
+
+ trace_data = [
+ ["Krok", "Czytam", "Stan przed", "Przejście", "Stan po"],
+ ["—", "—", "q₀ (start)", "—", "q₀"],
+ ["1", "b", "q₀", "δ(q₀, b) = q₀", "q₀"],
+ ["2", "a", "q₀", "δ(q₀, a) = q₁", "q₁"],
+ ["3", "a", "q₁", "δ(q₁, a) = q₁", "q₁"],
+ ["4", "b", "q₁", "δ(q₁, b) = q₂", "q₂ ✓"],
+ ]
+
+ colors = [GRAY2] + ["white"] * 4 + [LIGHT_GREEN]
+ table = ax2.table(
+ cellText=trace_data,
+ cellLoc="center",
+ loc="center",
+ bbox=[0.05, 0.15, 0.9, 0.75],
+ )
+ table.auto_set_font_size(False)
+ table.set_fontsize(FS)
+ for (row, _col), cell in table.get_celld().items():
+ cell.set_edgecolor(GRAY3)
+ if row == 0:
+ cell.set_facecolor(GRAY2)
+ cell.set_text_props(fontweight="bold")
+ else:
+ cell.set_facecolor(colors[row])
+ cell.set_height(0.12)
+
+ ax2.text(
+ 0.5,
+ 0.05,
+ 'Wynik: q₂ ∈ F → "baab" AKCEPTOWANE ✓',
+ ha="center",
+ va="center",
+ fontsize=FS + 1,
+ fontweight="bold",
+ transform=ax2.transAxes,
+ bbox={"boxstyle": "round,pad=0.4", "facecolor": LIGHT_GREEN, "edgecolor": LN},
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "fa_recognition_example.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ fa_recognition_example.png")
+
+
+# ============================================================
+# 2. PDA Recognition Example — PDA for aⁿbⁿ
+# ============================================================
+def draw_pda_recognition() -> None:
+ """PDA state diagram + step-by-step trace with stack visualization for 'aabb'."""
+ _fig, axes = plt.subplots(
+ 1, 2, figsize=(11.69, 5.5), gridspec_kw={"width_ratios": [1, 1.4]}
+ )
+
+ # --- Left: State diagram ---
+ ax = axes[0]
+ ax.set_xlim(-1, 5.5)
+ ax.set_ylim(-2, 3)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "PDA — diagram stanów\nL = {aⁿbⁿ | n ≥ 1}",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ R = 0.38
+ states = {"q₀": (0.8, 0.5), "q₁": (2.8, 0.5), "q₂": (4.8, 0.5)}
+
+ draw_state_circle(ax, *states["q₀"], R, "q₀", initial=True)
+ draw_state_circle(ax, *states["q₁"], R, "q₁")
+ draw_state_circle(ax, *states["q₂"], R, "q₂", accepting=True, fillcolor=LIGHT_GREEN)
+
+ # q₀ --b,A/ε--> q₁
+ draw_curved_arrow(
+ ax,
+ states["q₀"][0] + R,
+ states["q₀"][1],
+ states["q₁"][0] - R,
+ states["q₁"][1],
+ "b, A → ε\n(pop A)",
+ connectionstyle="arc3,rad=0.0",
+ label_offset=(0, 0.4),
+ )
+ # q₁ --ε,Z₀/Z₀--> q₂
+ draw_curved_arrow(
+ ax,
+ states["q₁"][0] + R,
+ states["q₁"][1],
+ states["q₂"][0] - R,
+ states["q₂"][1],
+ "ε, Z₀ → Z₀\n(akceptuj)",
+ connectionstyle="arc3,rad=0.0",
+ label_offset=(0, 0.45),
+ )
+ # q₀ self-loop: a, Z₀/AZ₀ and a, A/AA
+ draw_self_loop(
+ ax, *states["q₀"], R, "a, Z₀ → AZ₀\na, A → AA\n(push A)", direction="top"
+ )
+ # q₁ self-loop: b, A/ε
+ draw_self_loop(ax, *states["q₁"], R, "b, A → ε\n(pop A)", direction="top")
+
+ # Key explanation
+ ax.text(
+ 0.3,
+ -1.3,
+ "Notacja: symbol_wejścia, szczyt_stosu → nowy_szczyt\n"
+ "ε = brak symbolu (przejście spontaniczne lub pusty stos)",
+ fontsize=FS_SMALL,
+ ha="left",
+ va="center",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # --- Right: Step trace with stack ---
+ ax2 = axes[1]
+ ax2.axis("off")
+ ax2.set_title(
+ 'Ślad wykonania z wizualizacją stosu — wejście: "aabb"',
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ trace_data = [
+ ["Krok", "Czytam", "Stan", "Stos (szczyt→)", "Operacja"],
+ ["start", "—", "q₀", "[Z₀]", "—"],
+ ["1", "a", "q₀", "[A, Z₀]", "push A"],
+ ["2", "a", "q₀", "[A, A, Z₀]", "push A"],
+ ["3", "b", "q₁", "[A, Z₀]", "pop A"],
+ ["4", "b", "q₁", "[Z₀]", "pop A"],
+ ["5", "ε", "q₂", "[Z₀]", "akceptuj!"],
+ ]
+
+ colors = [
+ GRAY2,
+ "white",
+ LIGHT_BLUE,
+ LIGHT_BLUE,
+ LIGHT_YELLOW,
+ LIGHT_YELLOW,
+ LIGHT_GREEN,
+ ]
+ table = ax2.table(
+ cellText=trace_data,
+ cellLoc="center",
+ loc="center",
+ bbox=[0.02, 0.08, 0.96, 0.82],
+ )
+ table.auto_set_font_size(False)
+ table.set_fontsize(FS)
+ for (row, _col), cell in table.get_celld().items():
+ cell.set_edgecolor(GRAY3)
+ if row == 0:
+ cell.set_facecolor(GRAY2)
+ cell.set_text_props(fontweight="bold")
+ else:
+ cell.set_facecolor(colors[row])
+ cell.set_height(0.11)
+
+ ax2.text(
+ 0.5,
+ 0.0,
+ 'Wynik: q₂ ∈ F, stos=[Z₀] → "aabb" AKCEPTOWANE ✓\n'
+ 'Intuicja: 2x push A (za "aa") + 2x pop A (za "bb") = stos pusty = OK',
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ transform=ax2.transAxes,
+ bbox={"boxstyle": "round,pad=0.4", "facecolor": LIGHT_GREEN, "edgecolor": LN},
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "pda_recognition_example.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ pda_recognition_example.png")
+
+
+# ============================================================
+# 3. LBA Recognition Example — LBA for aⁿbⁿcⁿ
+# ============================================================
+def draw_lba_recognition() -> None:
+ """LBA tape visualization showing marking rounds for 'aabbcc'."""
+ _fig, ax = plt.subplots(1, 1, figsize=(11.69, 6.5))
+ ax.set_xlim(-0.5, 12)
+ ax.set_ylim(-1, 10.5)
+ ax.axis("off")
+ ax.set_title(
+ "LBA — rozpoznawanie aⁿbⁿcⁿ (n=2)\n"
+ "Strategia: w każdej rundzie zaznacz jedno a, b, c",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ CELL_W = 0.9
+ CELL_H = 0.7
+ TAPE_X0 = 1.5
+ HEAD_COLOR = "#FFD700"
+
+ def draw_tape(y, cells, head_pos, label, step_label="") -> None:
+ """Draw a tape row with cells, head position highlighted."""
+ ax.text(
+ 0.2,
+ y + CELL_H / 2,
+ label,
+ ha="right",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ for i, (sym, color) in enumerate(cells):
+ x = TAPE_X0 + i * CELL_W
+ fc = HEAD_COLOR if i == head_pos else color
+ rect = mpatches.FancyBboxPatch(
+ (x, y),
+ CELL_W,
+ CELL_H,
+ boxstyle="round,pad=0.03",
+ lw=1.2,
+ edgecolor=LN,
+ facecolor=fc,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + CELL_W / 2,
+ y + CELL_H / 2,
+ sym,
+ ha="center",
+ va="center",
+ fontsize=FS + 2,
+ fontweight="bold" if sym in ("X", "Y", "Z") else "normal",
+ family="monospace",
+ )
+ if head_pos is not None:
+ hx = TAPE_X0 + head_pos * CELL_W + CELL_W / 2
+ ax.annotate(
+ "▼",
+ xy=(hx, y + CELL_H),
+ xytext=(hx, y + CELL_H + 0.25),
+ ha="center",
+ va="bottom",
+ fontsize=8,
+ color="black",
+ )
+ if step_label:
+ sx = TAPE_X0 + 6 * CELL_W + 0.5
+ ax.text(
+ sx,
+ y + CELL_H / 2,
+ step_label,
+ ha="left",
+ va="center",
+ fontsize=FS_SMALL,
+ bbox={
+ "boxstyle": "round,pad=0.2",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ },
+ )
+
+ W = "white"
+ MK = GRAY1 # marked cell color
+
+ # Row 1: Initial tape
+ y = 9.0
+ draw_tape(
+ y,
+ [("a", W), ("a", W), ("b", W), ("b", W), ("c", W), ("c", W)],
+ 0,
+ "Początek",
+ "taśma = [a, a, b, b, c, c], głowica na 0",
+ )
+
+ # Row 2: After marking first 'a'
+ y = 7.8
+ draw_tape(
+ y,
+ [("X", MK), ("a", W), ("b", W), ("b", W), ("c", W), ("c", W)],
+ 1,
+ "R1, krok 1",
+ "zaznacz a→X, szukaj b",
+ )
+
+ # Row 3: After marking first 'b'
+ y = 6.6
+ draw_tape(
+ y,
+ [("X", MK), ("a", W), ("Y", MK), ("b", W), ("c", W), ("c", W)],
+ 3,
+ "R1, krok 2",
+ "zaznacz b→Y, szukaj c",
+ )
+
+ # Row 4: After marking first 'c'
+ y = 5.4
+ draw_tape(
+ y,
+ [("X", MK), ("a", W), ("Y", MK), ("b", W), ("Z", MK), ("c", W)],
+ 0,
+ "R1, krok 3",
+ "zaznacz c→Z, wróć na początek",
+ )
+
+ # Runda 2 header
+ y = 4.5
+ ax.text(
+ TAPE_X0 + 3 * CELL_W,
+ y + 0.3,
+ "═══ RUNDA 2 ═══",
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ color=LN,
+ )
+
+ # Row 5: After marking second 'a'
+ y = 3.6
+ draw_tape(
+ y,
+ [("X", MK), ("X", MK), ("Y", MK), ("b", W), ("Z", MK), ("c", W)],
+ 2,
+ "R2, krok 1",
+ "pomiń X, zaznacz a→X, szukaj b",
+ )
+
+ # Row 6: After marking second 'b'
+ y = 2.4
+ draw_tape(
+ y,
+ [("X", MK), ("X", MK), ("Y", MK), ("Y", MK), ("Z", MK), ("c", W)],
+ 4,
+ "R2, krok 2",
+ "pomiń Y, zaznacz b→Y, szukaj c",
+ )
+
+ # Row 7: After marking second 'c'
+ y = 1.2
+ draw_tape(
+ y,
+ [("X", MK), ("X", MK), ("Y", MK), ("Y", MK), ("Z", MK), ("Z", MK)],
+ None,
+ "R2, krok 3",
+ "zaznacz c→Z, wróć na początek",
+ )
+
+ # Result
+ y = 0.0
+ ax.text(
+ TAPE_X0 + 3 * CELL_W,
+ y + 0.3,
+ 'Wszystko zaznaczone → q_acc → "aabbcc" AKCEPTOWANE ✓',
+ ha="center",
+ va="center",
+ fontsize=FS + 1,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.4", "facecolor": LIGHT_GREEN, "edgecolor": LN},
+ )
+
+ # Key
+ ax.text(
+ TAPE_X0 + 6 * CELL_W + 0.5,
+ y + 0.3,
+ 'Ograniczenie LBA:\ngłowica ≤ 6 komórek\n(= |w| = |"aabbcc"|)',
+ ha="left",
+ va="center",
+ fontsize=FS_SMALL,
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": LIGHT_YELLOW,
+ "edgecolor": GRAY3,
+ },
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "lba_recognition_example.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ lba_recognition_example.png")
+
+
+# ============================================================
+# 4. TM Recognition Example — TM for 0ⁿ1ⁿ
+# ============================================================
+def draw_tm_recognition() -> None:
+ """TM tape visualization for 0ⁿ1ⁿ with infinite tape shown."""
+ _fig, ax = plt.subplots(1, 1, figsize=(11.69, 6.5))
+ ax.set_xlim(-0.5, 13)
+ ax.set_ylim(-1, 10.5)
+ ax.axis("off")
+ ax.set_title(
+ "TM — rozpoznawanie 0ⁿ1ⁿ (n=2)\n"
+ "Strategia: zaznacz jedno 0 i jedno 1 w każdej rundzie",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ CELL_W = 0.9
+ CELL_H = 0.7
+ TAPE_X0 = 1.5
+ HEAD_COLOR = "#FFD700"
+
+ def draw_tape(y, cells, head_pos, label, step_label="") -> None:
+ """Draw tape."""
+ ax.text(
+ 0.2,
+ y + CELL_H / 2,
+ label,
+ ha="right",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ for i, (sym, color) in enumerate(cells):
+ x = TAPE_X0 + i * CELL_W
+ fc = HEAD_COLOR if i == head_pos else color
+ lw = 1.2
+ ls = "-"
+ if sym == "⊔":
+ ls = "--"
+ rect = mpatches.FancyBboxPatch(
+ (x, y),
+ CELL_W,
+ CELL_H,
+ boxstyle="round,pad=0.03",
+ lw=lw,
+ edgecolor=LN,
+ facecolor=fc,
+ linestyle=ls,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + CELL_W / 2,
+ y + CELL_H / 2,
+ sym,
+ ha="center",
+ va="center",
+ fontsize=FS + 2,
+ fontweight="bold" if sym in ("X", "Y") else "normal",
+ family="monospace",
+ color=GRAY3 if sym == "⊔" else LN,
+ )
+ # ∞ arrow
+ last_x = TAPE_X0 + len(cells) * CELL_W
+ ax.annotate(
+ "→ ∞",
+ xy=(last_x + 0.3, y + CELL_H / 2),
+ fontsize=FS,
+ ha="left",
+ va="center",
+ color=GRAY3,
+ )
+ if head_pos is not None:
+ hx = TAPE_X0 + head_pos * CELL_W + CELL_W / 2
+ ax.annotate(
+ "▼",
+ xy=(hx, y + CELL_H),
+ xytext=(hx, y + CELL_H + 0.25),
+ ha="center",
+ va="bottom",
+ fontsize=8,
+ color="black",
+ )
+ if step_label:
+ sx = TAPE_X0 + 8 * CELL_W + 0.8
+ ax.text(
+ sx,
+ y + CELL_H / 2,
+ step_label,
+ ha="left",
+ va="center",
+ fontsize=FS_SMALL,
+ bbox={
+ "boxstyle": "round,pad=0.2",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ },
+ )
+
+ W = "white"
+ MK = GRAY1
+ BL = "#F0F0F0" # blank cell
+
+ # Row 1: Initial
+ y = 9.0
+ draw_tape(
+ y,
+ [("0", W), ("0", W), ("1", W), ("1", W), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
+ 0,
+ "Początek",
+ "taśma = [0,0,1,1,⊔,⊔,...∞]",
+ )
+
+ # Row 2: Mark first 0
+ y = 7.8
+ draw_tape(
+ y,
+ [("X", MK), ("0", W), ("1", W), ("1", W), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
+ 1,
+ "R1, krok 1",
+ "zaznacz 0→X, idź w prawo",
+ )
+
+ # Row 3: Skip to first 1, mark it
+ y = 6.6
+ draw_tape(
+ y,
+ [("X", MK), ("0", W), ("Y", MK), ("1", W), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
+ 0,
+ "R1, krok 2",
+ "zaznacz 1→Y, wróć na początek",
+ )
+
+ # Runda 2 header
+ y = 5.8
+ ax.text(
+ TAPE_X0 + 3.5 * CELL_W,
+ y + 0.3,
+ "═══ RUNDA 2 ═══",
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Row 4: Mark second 0
+ y = 4.8
+ draw_tape(
+ y,
+ [("X", MK), ("X", MK), ("Y", MK), ("1", W), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
+ 2,
+ "R2, krok 1",
+ "pomiń X, zaznacz 0→X",
+ )
+
+ # Row 5: Mark second 1
+ y = 3.6
+ draw_tape(
+ y,
+ [("X", MK), ("X", MK), ("Y", MK), ("Y", MK), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
+ 0,
+ "R2, krok 2",
+ "pomiń Y, zaznacz 1→Y, wróć",
+ )
+
+ # Row 6: Check — all marked
+ y = 2.4
+ draw_tape(
+ y,
+ [("X", MK), ("X", MK), ("Y", MK), ("Y", MK), ("⊔", BL), ("⊔", BL), ("⊔", BL)],
+ None,
+ "Sprawdzenie",
+ "brak niezaznaczonych → q_acc",
+ )
+
+ # Result + TM vs LBA comparison
+ y = 0.8
+ ax.text(
+ TAPE_X0 + 3.5 * CELL_W,
+ y + 0.3,
+ '"0011" AKCEPTOWANE ✓',
+ ha="center",
+ va="center",
+ fontsize=FS + 1,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.4", "facecolor": LIGHT_GREEN, "edgecolor": LN},
+ )
+
+ y = -0.3
+ ax.text(
+ TAPE_X0 + 3.5 * CELL_W,
+ y + 0.3,
+ "Różnica TM vs LBA: taśma TM jest nieskończona (⊔ → ∞)\n"
+ "LBA: głowica ograniczona do |w| komórek\n"
+ "TM: głowica może wyjść POZA wejście i pisać na pustych ⊔",
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ bbox={
+ "boxstyle": "round,pad=0.4",
+ "facecolor": LIGHT_YELLOW,
+ "edgecolor": GRAY3,
+ },
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "tm_recognition_example.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ tm_recognition_example.png")
+
+
+# ============================================================
+# Main
+# ============================================================
+if __name__ == "__main__":
+ print("Generating automata diagrams for PYTANIE 1...")
+ draw_fa_recognition()
+ draw_pda_recognition()
+ draw_lba_recognition()
+ draw_tm_recognition()
+ print(f"\nAll diagrams saved to {OUTPUT_DIR}/")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_bf_negative_diagram.py b/python_pkg/praca_magisterska_video/generate_images/generate_bf_negative_diagram.py
new file mode 100755
index 0000000..34a70e6
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_bf_negative_diagram.py
@@ -0,0 +1,598 @@
+#!/usr/bin/env python3
+"""Generate Bellman-Ford negative-weights & negative-cycle diagram for PYTANIE 2.
+
+Two-part figure:
+ Part 1: Graph with negative edge, Dijkstra WRONG vs Bellman-Ford CORRECT
+ Part 2: Negative cycle detection (add C→B(-3))
+
+A4-compatible, monochrome-friendly, 300 DPI.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 10
+FS_SMALL = 6.5
+FS_EDGE = 9
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+LIGHT_GREEN = "#D5E8D4"
+LIGHT_RED = "#F8D7DA"
+LIGHT_YELLOW = "#FFF9C4"
+
+# --- Graph layout for negative-weight example ---
+# S→A(2), A→C(3), S→B(5), B→A(-4)
+NEG_POS = {"S": (0.8, 2), "A": (3.3, 3.2), "B": (3.3, 0.8), "C": (5.8, 2)}
+NEG_EDGES = [("S", "A", 2), ("A", "C", 3), ("S", "B", 5), ("B", "A", -4)]
+
+
+def draw_node(
+ ax,
+ name,
+ pos,
+ color="white",
+ current=False,
+ visited=False,
+ dist_label=None,
+ fontsize=12,
+ error=False,
+) -> None:
+ """Draw node."""
+ x, y = pos
+ r = 0.35
+ lw = 2.5 if current else 1.5
+ ec = "#D32F2F" if current else ("#D32F2F" if error else LN)
+ fc = LIGHT_YELLOW if current else (LIGHT_GREEN if visited else color)
+ if error:
+ fc = LIGHT_RED
+
+ circle = plt.Circle(
+ (x, y), r, fill=True, facecolor=fc, edgecolor=ec, linewidth=lw, zorder=5
+ )
+ ax.add_patch(circle)
+ ax.text(
+ x,
+ y,
+ name,
+ ha="center",
+ va="center",
+ fontsize=fontsize,
+ fontweight="bold",
+ zorder=6,
+ )
+
+ if dist_label is not None:
+ bbox_ec = "#D32F2F" if error else GRAY3
+ bbox_fc = LIGHT_RED if error else "white"
+ ax.text(
+ x,
+ y - 0.55,
+ f"d={dist_label}",
+ ha="center",
+ va="center",
+ fontsize=FS,
+ zorder=6,
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": bbox_fc,
+ "edgecolor": bbox_ec,
+ "alpha": 0.95,
+ },
+ )
+
+
+def draw_edge(
+ ax,
+ pos1,
+ pos2,
+ weight,
+ highlighted=False,
+ relaxed=False,
+ negative=False,
+ cycle_edge=False,
+ offset=0.0,
+) -> None:
+ """Draw edge."""
+ x1, y1 = pos1
+ x2, y2 = pos2
+
+ dx, dy = x2 - x1, y2 - y1
+ length = np.sqrt(dx**2 + dy**2)
+ r = 0.38
+ sx = x1 + r * dx / length
+ sy = y1 + r * dy / length
+ ex = x2 - r * dx / length
+ ey = y2 - r * dy / length
+
+ # Offset perpendicular for parallel edges
+ if offset != 0:
+ perp_x = -dy / length * offset
+ perp_y = dx / length * offset
+ sx += perp_x
+ sy += perp_y
+ ex += perp_x
+ ey += perp_y
+
+ if cycle_edge:
+ color = "#D32F2F"
+ lw = 2.5
+ ls = "--"
+ elif negative or relaxed:
+ color = "#D32F2F"
+ lw = 2.5
+ ls = "-"
+ elif highlighted:
+ color = "#1565C0"
+ lw = 2.0
+ ls = "-"
+ else:
+ color = GRAY3
+ lw = 1.5
+ ls = "-"
+
+ # Arrow
+ ax.annotate(
+ "",
+ xy=(ex, ey),
+ xytext=(sx, sy),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": color,
+ "lw": lw,
+ "linestyle": ls,
+ "shrinkA": 0,
+ "shrinkB": 0,
+ },
+ zorder=2,
+ )
+
+ # Weight label
+ mx = (sx + ex) / 2
+ my = (sy + ey) / 2
+ perp_x = -dy / length * 0.22
+ perp_y = dx / length * 0.22
+ if offset != 0:
+ perp_x *= 0.5
+ perp_y *= 0.5
+
+ weight_str = str(weight)
+ edge_fc = LIGHT_RED if negative or cycle_edge else "white"
+ edge_ec = "#D32F2F" if negative or cycle_edge else GRAY3
+ ax.text(
+ mx + perp_x,
+ my + perp_y,
+ weight_str,
+ ha="center",
+ va="center",
+ fontsize=FS_EDGE,
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": edge_fc,
+ "edgecolor": edge_ec,
+ "alpha": 0.95,
+ },
+ zorder=4,
+ )
+
+
+def draw_neg_graph(
+ ax,
+ edges,
+ title="",
+ dist=None,
+ current=None,
+ visited=None,
+ relaxed_edges=None,
+ error_nodes=None,
+ extra_edges=None,
+ node_positions=None,
+) -> None:
+ """Draw neg graph."""
+ if visited is None:
+ visited = set()
+ if relaxed_edges is None:
+ relaxed_edges = set()
+ if dist is None:
+ dist = {}
+ if error_nodes is None:
+ error_nodes = set()
+ if node_positions is None:
+ node_positions = NEG_POS
+
+ ax.set_xlim(-0.5, 7.0)
+ ax.set_ylim(-0.8, 4.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ if title:
+ ax.set_title(title, fontsize=FS, fontweight="bold", pad=5)
+
+ all_edges = list(edges)
+ if extra_edges:
+ all_edges += extra_edges
+
+ for u, v, w in all_edges:
+ rl = (u, v) in relaxed_edges
+ neg = w < 0
+ cycle = extra_edges and (u, v, w) in extra_edges
+ # If B→A and A→B both exist, offset them
+ off = 0.0
+ draw_edge(
+ ax,
+ node_positions[u],
+ node_positions[v],
+ w,
+ relaxed=rl,
+ negative=neg,
+ cycle_edge=cycle,
+ offset=off,
+ )
+
+ for name, pos in node_positions.items():
+ is_current = name == current
+ is_visited = name in visited
+ d_label = dist.get(name)
+ is_error = name in error_nodes
+ draw_node(
+ ax,
+ name,
+ pos,
+ current=is_current,
+ visited=is_visited,
+ dist_label=d_label,
+ error=is_error,
+ )
+
+
+def generate_bf_negative_weights() -> None:
+ """Two-row figure.
+
+ Row 1: Graph structure + Dijkstra WRONG + Bellman-Ford CORRECT
+ Row 2: B-F iterations 1-3 step by step.
+ """
+ fig = plt.figure(figsize=(14, 10))
+ fig.suptitle(
+ "Bellman-Ford — ujemne wagi vs Dijkstra\n"
+ "Graf: S→A(2), A→C(3), S→B(5), B→A(-4). Start = S",
+ fontsize=FS_TITLE + 1,
+ fontweight="bold",
+ y=0.99,
+ )
+
+ # ---- Row 1: Graph + Dijkstra wrong + BF correct ----
+
+ # Panel 1: The graph structure
+ ax1 = fig.add_subplot(2, 3, 1)
+ draw_neg_graph(
+ ax1,
+ NEG_EDGES,
+ title="Graf z ujemną wagą\n(B→A = -4, zaznaczona na czerwono)",
+ dist={"S": "0", "A": "?", "B": "?", "C": "?"},
+ )
+ # START label
+ ax1.annotate(
+ "START",
+ xy=(NEG_POS["S"][0] - 0.35, NEG_POS["S"][1]),
+ xytext=(NEG_POS["S"][0] - 1.2, NEG_POS["S"][1]),
+ fontsize=FS,
+ fontweight="bold",
+ color="#D32F2F",
+ arrowprops={"arrowstyle": "->", "color": "#D32F2F", "lw": 2},
+ va="center",
+ )
+
+ # Panel 2: Dijkstra — WRONG
+ ax2 = fig.add_subplot(2, 3, 2)
+ draw_neg_graph(
+ ax2,
+ NEG_EDGES,
+ title="Dijkstra — BŁĘDNY wynik\nA zamknięty z d=2, nie poprawia przy B→A",
+ dist={"S": "0", "A": "2", "B": "5", "C": "5"},
+ visited={"S", "A", "B", "C"},
+ error_nodes={"A", "C"},
+ )
+ # Add "WRONG" annotations
+ ax2.text(
+ NEG_POS["A"][0] + 0.6,
+ NEG_POS["A"][1] + 0.3,
+ "✗ powinno 1",
+ fontsize=FS_SMALL,
+ color="#D32F2F",
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.1",
+ "facecolor": LIGHT_RED,
+ "edgecolor": "#D32F2F",
+ "alpha": 0.9,
+ "lw": 0.5,
+ },
+ )
+ ax2.text(
+ NEG_POS["C"][0] + 0.05,
+ NEG_POS["C"][1] + 0.55,
+ "✗ powinno 4",
+ fontsize=FS_SMALL,
+ color="#D32F2F",
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.1",
+ "facecolor": LIGHT_RED,
+ "edgecolor": "#D32F2F",
+ "alpha": 0.9,
+ "lw": 0.5,
+ },
+ )
+
+ # Panel 3: Bellman-Ford — CORRECT
+ ax3 = fig.add_subplot(2, 3, 3)
+ draw_neg_graph(
+ ax3,
+ NEG_EDGES,
+ title="Bellman-Ford — POPRAWNY wynik\nUjemna waga B→A poprawnie propagowana",
+ dist={"S": "0", "A": "1", "B": "5", "C": "4"},
+ visited={"S", "A", "B", "C"},
+ relaxed_edges={("B", "A")},
+ )
+ ax3.text(
+ NEG_POS["A"][0] + 0.6,
+ NEG_POS["A"][1] + 0.3,
+ "✓ poprawne!",
+ fontsize=FS_SMALL,
+ color="#006400",
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.1",
+ "facecolor": LIGHT_GREEN,
+ "edgecolor": "#006400",
+ "alpha": 0.9,
+ "lw": 0.5,
+ },
+ )
+ ax3.text(
+ NEG_POS["C"][0] + 0.05,
+ NEG_POS["C"][1] + 0.55,
+ "✓ poprawne!",
+ fontsize=FS_SMALL,
+ color="#006400",
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.1",
+ "facecolor": LIGHT_GREEN,
+ "edgecolor": "#006400",
+ "alpha": 0.9,
+ "lw": 0.5,
+ },
+ )
+
+ # ---- Row 2: B-F iterations step by step ----
+ iterations = [
+ {
+ "title": "B-F Iteracja 1\nRelaksuj WSZYSTKIE krawędzie",
+ "dist": {"S": "0", "A": "1", "B": "5", "C": "5"},
+ "relaxed": {("S", "A"), ("A", "C"), ("S", "B"), ("B", "A")},
+ "detail": (
+ "S→A: 0+2=2<∞ → A=2\n"
+ "A→C: 2+3=5<∞ → C=5\n"
+ "S→B: 0+5=5<∞ → B=5\n"
+ "B→A: 5-4=1<2 → A=1 ✓"
+ ),
+ },
+ {
+ "title": "B-F Iteracja 2\nPropagacja poprawionego A",
+ "dist": {"S": "0", "A": "1", "B": "5", "C": "4"},
+ "relaxed": {("A", "C")},
+ "detail": (
+ "S→A: 0+2=2>1 ✗\nA→C: 1+3=4<5 → C=4 ✓\nS→B: 0+5=5=5 ✗\nB→A: 5-4=1=1 ✗"
+ ),
+ },
+ {
+ "title": "B-F Iteracja 3\nBrak zmian → stabilne!",
+ "dist": {"S": "0", "A": "1", "B": "5", "C": "4"},
+ "relaxed": set(),
+ "detail": (
+ "Wszystkie krawędzie:\n"
+ "brak poprawy ✗\n"
+ "→ wynik stabilny\n"
+ "→ BRAK cyklu ujemnego"
+ ),
+ },
+ ]
+
+ for i, it in enumerate(iterations):
+ ax = fig.add_subplot(2, 3, i + 4)
+ draw_neg_graph(
+ ax,
+ NEG_EDGES,
+ title=it["title"],
+ dist=it["dist"],
+ visited={"S", "A", "B", "C"},
+ relaxed_edges=it["relaxed"],
+ )
+ # Detail text below graph
+ ax.text(
+ 3.2,
+ -0.5,
+ it["detail"],
+ ha="center",
+ va="top",
+ fontsize=FS_SMALL,
+ family="monospace",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # Bottom note
+ fig.text(
+ 0.5,
+ 0.01,
+ "Dijkstra zamyka wierzchołki na stałe (zachłanność) → ujemna waga B→A(-4) nie może poprawić zamkniętego A.\n"
+ "Bellman-Ford relaksuje WSZYSTKIE krawędzie w każdej iteracji → ujemne wagi propagują się poprawnie.",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": LIGHT_YELLOW, "edgecolor": LN},
+ )
+
+ plt.tight_layout(rect=[0, 0.05, 1, 0.95])
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "bellman_ford_negative_weights.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ bellman_ford_negative_weights.png")
+
+
+def generate_bf_negative_cycle() -> None:
+ """Figure showing negative cycle detection.
+
+ Graph: S→A(2), A→C(3), S→B(5), B→A(-4), C→B(-3) [added edge]
+ Cycle: B→A→C→B = -4+3+(-3) = -4 < 0.
+ """
+ fig = plt.figure(figsize=(14, 5.5))
+ fig.suptitle(
+ "Bellman-Ford — wykrywanie cyklu ujemnego\n"
+ "Dodano krawędź C→B(-3). Cykl: B→A→C→B = -4+3+(-3) = -4 < 0",
+ fontsize=FS_TITLE + 1,
+ fontweight="bold",
+ y=0.99,
+ )
+
+ # Panel 1: Graph with cycle highlighted
+ ax1 = fig.add_subplot(1, 3, 1)
+ draw_neg_graph(
+ ax1,
+ NEG_EDGES,
+ title="Graf z cyklem ujemnym\nDodana krawędź C→B(-3) — przerywana",
+ dist={"S": "0", "A": "?", "B": "?", "C": "?"},
+ extra_edges=[("C", "B", -3)],
+ )
+ # Mark cycle
+ ax1.annotate(
+ "CYKL\n-4+3+(-3)=-4<0",
+ xy=(3.3, 2.0),
+ fontsize=FS,
+ fontweight="bold",
+ color="#D32F2F",
+ ha="center",
+ va="center",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": LIGHT_RED,
+ "edgecolor": "#D32F2F",
+ "alpha": 0.9,
+ },
+ )
+
+ # Panel 2: After V-1 iterations — still changing
+ ax2 = fig.add_subplot(1, 3, 2)
+ draw_neg_graph(
+ ax2,
+ NEG_EDGES,
+ title="Po V-1=3 iteracjach\ndist wciąż maleje (niestabilne!)",
+ dist={"S": "0", "A": "-7", "B": "-4", "C": "-4"},
+ visited={"S", "A", "B", "C"},
+ error_nodes={"A", "B", "C"},
+ extra_edges=[("C", "B", -3)],
+ )
+ ax2.text(
+ 3.2,
+ -0.4,
+ "Każde okrążenie cyklu\nzmniejsza dist o 4.\nDist → -∞ (brak minimum!)",
+ ha="center",
+ va="top",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": LIGHT_RED,
+ "edgecolor": "#D32F2F",
+ },
+ )
+
+ # Panel 3: V-th iteration detects
+ ax3 = fig.add_subplot(1, 3, 3)
+ ax3.axis("off")
+ ax3.set_xlim(0, 10)
+ ax3.set_ylim(0, 10)
+
+ detection_text = (
+ "V-ta iteracja (sprawdzenie):\n"
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
+ "for (src, dst, w) in edges:\n"
+ " if dist[src]+w < dist[dst]:\n"
+ " return None # CYKL!\n\n"
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
+ "Sprawdzamy np. krawędź B→A:\n"
+ " dist[B] + (-4) = -4 + (-4) = -8\n"
+ " -8 < dist[A] = -7\n"
+ " → NADAL SIĘ POPRAWIA!\n"
+ " → CYKL UJEMNY WYKRYTY!\n\n"
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
+ "Wynik: return None\n"
+ "(najkrótsza ścieżka nie istnieje)"
+ )
+ ax3.text(
+ 5,
+ 5,
+ detection_text,
+ ha="center",
+ va="center",
+ fontsize=FS + 0.5,
+ family="monospace",
+ bbox={
+ "boxstyle": "round,pad=0.6",
+ "facecolor": LIGHT_RED,
+ "edgecolor": "#D32F2F",
+ "lw": 2,
+ },
+ )
+ ax3.set_title(
+ "Wykrywanie — V-ta iteracja\nJeśli cokolwiek się poprawia → cykl ujemny!",
+ fontsize=FS,
+ fontweight="bold",
+ pad=5,
+ )
+
+ # Bottom note
+ fig.text(
+ 0.5,
+ 0.01,
+ "Bez cyklu ujemnego: po V-1 iteracjach dist jest stabilne. "
+ "Z cyklem ujemnym: dist maleje w nieskończoność → V-ta iteracja to wykrywa.",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": LIGHT_YELLOW, "edgecolor": LN},
+ )
+
+ plt.tight_layout(rect=[0, 0.06, 1, 0.94])
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "bellman_ford_negative_cycle.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ bellman_ford_negative_cycle.png")
+
+
+if __name__ == "__main__":
+ print("Generating Bellman-Ford negative weight diagrams...")
+ generate_bf_negative_weights()
+ generate_bf_negative_cycle()
+ print(f"\nAll diagrams saved to {OUTPUT_DIR}/")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_normalization_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_normalization_diagrams.py
new file mode 100755
index 0000000..ccc0e3e
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_normalization_diagrams.py
@@ -0,0 +1,1168 @@
+#!/usr/bin/env python3
+"""Generate B&W normalization step diagrams for PYTANIE 3.
+
+Each diagram shows database tables at a specific normalization stage.
+Designed for A4 laser printer output (300 DPI, black & white).
+"""
+
+from __future__ import annotations
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+import matplotlib.patches as mpatches
+import matplotlib.pyplot as plt
+
+if TYPE_CHECKING:
+ from matplotlib.axes import Axes
+ from matplotlib.figure import Figure
+
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+# Common settings
+DPI = 300
+FONT_SIZE = 8
+HEADER_COLOR = "#D0D0D0"
+CELL_COLOR = "#FFFFFF"
+HIGHLIGHT_COLOR = "#F0D0D0" # light red-ish gray for violations
+FIXED_COLOR = "#D0F0D0" # light green-ish gray for fixed
+FD_ARROW_COLOR = "#444444"
+
+
+def draw_table(
+ ax,
+ x,
+ y,
+ title,
+ headers,
+ rows,
+ col_widths=None,
+ highlight_cols=None,
+ highlight_rows=None,
+ highlight_cells=None,
+ strikethrough_cells=None,
+ title_fontsize=9,
+) -> tuple[float, float]:
+ """Draw a single table on the axes at position (x, y).
+
+ Args:
+ ax: matplotlib axes
+ x: left position of the table
+ y: top position of the table
+ title: table title string
+ headers: list of column header strings
+ rows: list of lists (row data)
+ col_widths: list of column widths (in inches-ish units)
+ highlight_cols: set of column indices to highlight
+ highlight_rows: set of row indices to highlight
+ highlight_cells: set of (row, col) to highlight
+ strikethrough_cells: set of (row, col) to draw strikethrough
+ title_fontsize: font size for table title
+
+ Returns:
+ (width, height) of the drawn table
+ """
+ n_cols = len(headers)
+ n_rows = len(rows)
+
+ if col_widths is None:
+ # Auto-calculate based on content
+ col_widths = []
+ for c in range(n_cols):
+ max_len = len(headers[c])
+ for r in rows:
+ if c < len(r):
+ max_len = max(max_len, len(str(r[c])))
+ col_widths.append(max(max_len * 0.08 + 0.1, 0.5))
+
+ row_height = 0.22
+ total_width = sum(col_widths)
+ total_height = (n_rows + 1) * row_height # +1 for header
+
+ # Title
+ ax.text(
+ x + total_width / 2,
+ y + 0.18,
+ title,
+ fontsize=title_fontsize,
+ fontweight="bold",
+ ha="center",
+ va="bottom",
+ family="monospace",
+ )
+
+ y_start = y
+
+ # Draw header row
+ cx = x
+ for _c, (hdr, w) in enumerate(zip(headers, col_widths, strict=False)):
+ color = HEADER_COLOR
+ rect = mpatches.FancyBboxPatch(
+ (cx, y_start),
+ w,
+ -row_height,
+ boxstyle="square,pad=0",
+ facecolor=color,
+ edgecolor="black",
+ linewidth=0.5,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ cx + w / 2,
+ y_start - row_height / 2,
+ hdr,
+ fontsize=FONT_SIZE,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ family="monospace",
+ )
+ cx += w
+
+ # Draw data rows
+ for r_idx, row in enumerate(rows):
+ cy = y_start - (r_idx + 1) * row_height
+ cx = x
+ for c_idx, (val, w) in enumerate(zip(row, col_widths, strict=False)):
+ color = CELL_COLOR
+ if highlight_cols and c_idx in highlight_cols:
+ color = HIGHLIGHT_COLOR
+ if highlight_rows and r_idx in highlight_rows:
+ color = HIGHLIGHT_COLOR
+ if highlight_cells and (r_idx, c_idx) in highlight_cells:
+ color = HIGHLIGHT_COLOR
+
+ rect = mpatches.FancyBboxPatch(
+ (cx, cy),
+ w,
+ -row_height,
+ boxstyle="square,pad=0",
+ facecolor=color,
+ edgecolor="black",
+ linewidth=0.5,
+ )
+ ax.add_patch(rect)
+
+ text_color = "black"
+ ax.text(
+ cx + w / 2,
+ cy - row_height / 2,
+ str(val),
+ fontsize=FONT_SIZE,
+ ha="center",
+ va="center",
+ family="monospace",
+ color=text_color,
+ )
+
+ if strikethrough_cells and (r_idx, c_idx) in strikethrough_cells:
+ ax.plot(
+ [cx + 0.03, cx + w - 0.03],
+ [cy - row_height / 2, cy - row_height / 2],
+ color="black",
+ linewidth=1.0,
+ )
+
+ cx += w
+
+ return total_width, total_height + 0.25 # extra for title
+
+
+def create_figure(width_inches=11.69, height_inches=8.27) -> tuple[Figure, Axes]:
+ """Create A4 landscape figure."""
+ fig, ax = plt.subplots(1, 1, figsize=(width_inches, height_inches), dpi=DPI)
+ ax.set_xlim(0, width_inches)
+ ax.set_ylim(0, height_inches)
+ ax.axis("off")
+ ax.set_aspect("equal")
+ return fig, ax
+
+
+def add_arrow(ax, x1, y1, x2, y2, label="", color="black") -> None:
+ """Draw an arrow with optional label."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": "->", "color": color, "lw": 1.5},
+ )
+ if label:
+ mx, my = (x1 + x2) / 2, (y1 + y2) / 2
+ ax.text(
+ mx,
+ my + 0.12,
+ label,
+ fontsize=7,
+ ha="center",
+ va="bottom",
+ family="monospace",
+ color=color,
+ )
+
+
+def add_label(
+ ax, x, y, text, fontsize=8, color="black", ha="left", style="normal"
+) -> None:
+ """Add a text label."""
+ ax.text(
+ x,
+ y,
+ text,
+ fontsize=fontsize,
+ ha=ha,
+ va="center",
+ family="monospace",
+ color=color,
+ style=style,
+ )
+
+
+# ============================================================
+# DIAGRAM 1: 0NF Table
+# ============================================================
+def draw_0nf() -> None:
+ """Draw 0nf."""
+ fig, ax = create_figure(11.69, 5.5)
+
+ headers = [
+ "StID",
+ "Imie",
+ "Telefony",
+ "KursID",
+ "NazwaKursu",
+ "Prowadzacy",
+ "WydzialID",
+ "NazwaWydzialu",
+ ]
+ rows = [
+ [
+ "1",
+ "Anna",
+ "111-222, 333-444",
+ "K10",
+ "Bazy danych",
+ "Kowalski",
+ "W4",
+ "EiTI",
+ ],
+ ["1", "Anna", "111-222, 333-444", "K20", "Algorytmy", "Nowak", "W4", "EiTI"],
+ ["2", "Jan", "555-666", "K10", "Bazy danych", "Kowalski", "W4", "EiTI"],
+ ["3", "Ewa", "777-888", "K30", "Optyka", "Wisniewski", "W2", "Fizyka"],
+ ]
+ col_widths = [0.5, 0.55, 1.55, 0.65, 1.1, 1.05, 0.85, 1.2]
+
+ # Highlight the non-atomic column
+ draw_table(
+ ax,
+ 0.8,
+ 4.5,
+ "0NF: Rejestr (forma nienormalna)",
+ headers,
+ rows,
+ col_widths,
+ highlight_cols={2}, # Telefony column
+ title_fontsize=11,
+ )
+
+ # Annotations
+ add_label(
+ ax,
+ 0.8,
+ 1.9,
+ 'PROBLEM: Kolumna "Telefony" zawiera LISTY wartosci (nieatomowe).',
+ fontsize=9,
+ color="black",
+ )
+ add_label(
+ ax,
+ 0.8,
+ 1.55,
+ 'Redundancja: "Anna", "W4", "EiTI", "Bazy danych" powtorzone wielokrotnie.',
+ fontsize=9,
+ color="black",
+ )
+ add_label(
+ ax,
+ 0.8,
+ 1.2,
+ "Zaleznosci funkcyjne: StID -> Imie, WydzialID | WydzialID -> NazwaWydzialu",
+ fontsize=8,
+ color="#333333",
+ )
+ add_label(
+ ax,
+ 0.8,
+ 0.9,
+ " KursID -> NazwaKursu | (StID,KursID) -> Prowadzacy | Prowadzacy -> KursID",
+ fontsize=8,
+ color="#333333",
+ )
+
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "nf_0nf_table.png"),
+ bbox_inches="tight",
+ facecolor="white",
+ pad_inches=0.2,
+ )
+ plt.close(fig)
+ print("Generated: nf_0nf_table.png")
+
+
+# ============================================================
+# DIAGRAM 2: 1NF — atomic values
+# ============================================================
+def draw_1nf() -> None:
+ """Draw 1nf."""
+ fig, ax = create_figure(11.69, 6.0)
+
+ # Main table after removing Telefony
+ headers1 = [
+ "StID*",
+ "Imie",
+ "KursID*",
+ "NazwaKursu",
+ "Prowadzacy",
+ "WydzialID",
+ "NazwaWydzialu",
+ ]
+ rows1 = [
+ ["1", "Anna", "K10", "Bazy danych", "Kowalski", "W4", "EiTI"],
+ ["1", "Anna", "K20", "Algorytmy", "Nowak", "W4", "EiTI"],
+ ["2", "Jan", "K10", "Bazy danych", "Kowalski", "W4", "EiTI"],
+ ["3", "Ewa", "K30", "Optyka", "Wisniewski", "W2", "Fizyka"],
+ ]
+ cw1 = [0.55, 0.55, 0.7, 1.1, 1.05, 0.85, 1.2]
+
+ draw_table(
+ ax,
+ 0.5,
+ 5.2,
+ "1NF: Rejestr (klucz: StID, KursID)",
+ headers1,
+ rows1,
+ cw1,
+ title_fontsize=10,
+ )
+
+ # Telefony table
+ headers2 = ["StID*", "Telefon*"]
+ rows2 = [
+ ["1", "111-222"],
+ ["1", "333-444"],
+ ["2", "555-666"],
+ ["3", "777-888"],
+ ]
+ cw2 = [0.55, 0.85]
+
+ draw_table(
+ ax,
+ 7.5,
+ 5.2,
+ "Telefony (klucz: StID, Telefon)",
+ headers2,
+ rows2,
+ cw2,
+ title_fontsize=10,
+ )
+
+ # Arrow
+ add_arrow(ax, 6.6, 4.3, 7.4, 4.3, "wydzielono", "#333333")
+
+ # Annotations
+ add_label(
+ ax,
+ 0.5,
+ 2.6,
+ 'KROK: Nieatomowa kolumna "Telefony" wydzielona do osobnej tabeli.',
+ fontsize=9,
+ )
+ add_label(
+ ax,
+ 0.5,
+ 2.25,
+ "Kazda komorka zawiera JEDNA wartosc. Klucz glowny wyznaczony.",
+ fontsize=9,
+ )
+ add_label(
+ ax,
+ 0.5,
+ 1.85,
+ "PROBLEM 2NF: NazwaKursu zalezy TYLKO od KursID (czesc klucza).",
+ fontsize=9,
+ color="black",
+ )
+ add_label(
+ ax,
+ 0.5,
+ 1.5,
+ " Imie, WydzialID, NazwaWydzialu zaleza TYLKO od StID (czesc klucza).",
+ fontsize=9,
+ color="black",
+ )
+ add_label(
+ ax,
+ 0.5,
+ 1.15,
+ " --> Czesciowe zaleznosci od klucza zlozonego = NARUSZENIE 2NF.",
+ fontsize=9,
+ color="black",
+ )
+
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "nf_1nf_tables.png"),
+ bbox_inches="tight",
+ facecolor="white",
+ pad_inches=0.2,
+ )
+ plt.close(fig)
+ print("Generated: nf_1nf_tables.png")
+
+
+# ============================================================
+# DIAGRAM 3: 2NF — no partial dependencies
+# ============================================================
+def draw_2nf() -> None:
+ """Draw 2nf."""
+ fig, ax = create_figure(11.69, 6.5)
+
+ # Studenci
+ h1 = ["StID*", "Imie", "WydzialID", "NazwaWydzialu"]
+ r1 = [
+ ["1", "Anna", "W4", "EiTI"],
+ ["2", "Jan", "W4", "EiTI"],
+ ["3", "Ewa", "W2", "Fizyka"],
+ ]
+ cw1 = [0.55, 0.55, 0.85, 1.2]
+ draw_table(
+ ax,
+ 0.3,
+ 5.8,
+ "Studenci (kl: StID)",
+ h1,
+ r1,
+ cw1,
+ highlight_cols={2, 3},
+ title_fontsize=9,
+ )
+
+ # Kursy
+ h2 = ["KursID*", "NazwaKursu"]
+ r2 = [["K10", "Bazy danych"], ["K20", "Algorytmy"], ["K30", "Optyka"]]
+ cw2 = [0.7, 1.1]
+ draw_table(ax, 4.0, 5.8, "Kursy (kl: KursID)", h2, r2, cw2, title_fontsize=9)
+
+ # Zapisy
+ h3 = ["StID*", "KursID*", "Prowadzacy"]
+ r3 = [
+ ["1", "K10", "Kowalski"],
+ ["1", "K20", "Nowak"],
+ ["2", "K10", "Kowalski"],
+ ["3", "K30", "Wisniewski"],
+ ]
+ cw3 = [0.55, 0.7, 1.05]
+ draw_table(ax, 6.8, 5.8, "Zapisy (kl: StID, KursID)", h3, r3, cw3, title_fontsize=9)
+
+ # Telefony
+ h4 = ["StID*", "Telefon*"]
+ r4 = [["1", "111-222"], ["1", "333-444"], ["2", "555-666"], ["3", "777-888"]]
+ cw4 = [0.55, 0.85]
+ draw_table(ax, 9.5, 5.8, "Telefony", h4, r4, cw4, title_fontsize=9)
+
+ # Annotations
+ add_label(
+ ax,
+ 0.3,
+ 3.3,
+ "KROK: Rozbito czesc. zaleznosci — atrybuty zalezne od czesci klucza wydzielone.",
+ fontsize=9,
+ )
+ add_label(
+ ax,
+ 0.3,
+ 2.95,
+ " StID -> Imie, WydzialID, NazwaWydzialu ==> tabela Studenci",
+ fontsize=8,
+ color="#333333",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 2.65,
+ " KursID -> NazwaKursu ==> tabela Kursy",
+ fontsize=8,
+ color="#333333",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 2.3,
+ 'PROBLEM 3NF w "Studenci": StID -> WydzialID -> NazwaWydzialu',
+ fontsize=9,
+ color="black",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 1.95,
+ " NazwaWydzialu zalezy od WydzialID (nie-klucz), nie bezposrednio od StID.",
+ fontsize=9,
+ color="black",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 1.6,
+ " --> Zaleznosc PRZECHODNIA = NARUSZENIE 3NF.",
+ fontsize=9,
+ color="black",
+ )
+
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "nf_2nf_tables.png"),
+ bbox_inches="tight",
+ facecolor="white",
+ pad_inches=0.2,
+ )
+ plt.close(fig)
+ print("Generated: nf_2nf_tables.png")
+
+
+# ============================================================
+# DIAGRAM 4: 3NF — no transitive dependencies
+# ============================================================
+def draw_3nf() -> None:
+ """Draw 3nf."""
+ fig, ax = create_figure(11.69, 6.5)
+
+ # Studenci (fixed)
+ h1 = ["StID*", "Imie", "WydzialID"]
+ r1 = [["1", "Anna", "W4"], ["2", "Jan", "W4"], ["3", "Ewa", "W2"]]
+ cw1 = [0.55, 0.55, 0.85]
+ draw_table(ax, 0.3, 5.8, "Studenci (kl: StID)", h1, r1, cw1, title_fontsize=9)
+
+ # Wydzialy (new!)
+ h2 = ["WydzialID*", "NazwaWydzialu"]
+ r2 = [["W4", "EiTI"], ["W2", "Fizyka"]]
+ cw2 = [0.85, 1.2]
+ draw_table(ax, 2.6, 5.8, "Wydzialy (kl: WydzialID)", h2, r2, cw2, title_fontsize=9)
+
+ # Kursy
+ h3 = ["KursID*", "NazwaKursu"]
+ r3 = [["K10", "Bazy danych"], ["K20", "Algorytmy"], ["K30", "Optyka"]]
+ cw3 = [0.7, 1.1]
+ draw_table(ax, 5.2, 5.8, "Kursy (kl: KursID)", h3, r3, cw3, title_fontsize=9)
+
+ # Zapisy (highlight BCNF violation)
+ h4 = ["StID*", "KursID*", "Prowadzacy"]
+ r4 = [
+ ["1", "K10", "Kowalski"],
+ ["1", "K20", "Nowak"],
+ ["2", "K10", "Kowalski"],
+ ["3", "K30", "Wisniewski"],
+ ]
+ cw4 = [0.55, 0.7, 1.05]
+ draw_table(
+ ax,
+ 7.8,
+ 5.8,
+ "Zapisy (kl: StID, KursID)",
+ h4,
+ r4,
+ cw4,
+ highlight_cols={1, 2},
+ title_fontsize=9,
+ )
+
+ # Annotations
+ add_label(
+ ax,
+ 0.3,
+ 3.3,
+ "KROK: Rozdzielono Studenci -> Studenci + Wydzialy (usun. zal. przechodnia).",
+ fontsize=9,
+ )
+ add_label(
+ ax,
+ 0.3,
+ 2.95,
+ " StID -> WydzialID -> NazwaWydzialu rozbito: NazwaWydzialu w osobnej tabeli.",
+ fontsize=8,
+ color="#333333",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 2.55,
+ 'PROBLEM BCNF w "Zapisy": FD: Prowadzacy -> KursID (1 prowadzacy = 1 kurs)',
+ fontsize=9,
+ color="black",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 2.2,
+ " Prowadzacy NIE jest nadkluczem tabeli Zapisy -> NARUSZENIE BCNF.",
+ fontsize=9,
+ color="black",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 1.85,
+ " 3NF OK, bo KursID jest atrybutem pierwszym (prime) -> wyjatek 3NF.",
+ fontsize=9,
+ color="#333333",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 1.5,
+ " BCNF nie ma takiego wyjatku -> kazda nietrywialna FD wymaga nadklucza po lewej.",
+ fontsize=9,
+ color="#333333",
+ )
+
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "nf_3nf_tables.png"),
+ bbox_inches="tight",
+ facecolor="white",
+ pad_inches=0.2,
+ )
+ plt.close(fig)
+ print("Generated: nf_3nf_tables.png")
+
+
+# ============================================================
+# DIAGRAM 5: BCNF — every determinant is a superkey
+# ============================================================
+def draw_bcnf() -> None:
+ """Draw bcnf."""
+ fig, ax = create_figure(11.69, 7.5)
+
+ # Studenci
+ h1 = ["StID*", "Imie", "WydzialID"]
+ r1 = [["1", "Anna", "W4"], ["2", "Jan", "W4"], ["3", "Ewa", "W2"]]
+ cw1 = [0.55, 0.55, 0.85]
+ draw_table(ax, 0.3, 6.8, "Studenci", h1, r1, cw1, title_fontsize=9)
+
+ # Wydzialy
+ h2 = ["WydzialID*", "NazwaWydz."]
+ r2 = [["W4", "EiTI"], ["W2", "Fizyka"]]
+ cw2 = [0.85, 1.0]
+ draw_table(ax, 2.5, 6.8, "Wydzialy", h2, r2, cw2, title_fontsize=9)
+
+ # Kursy
+ h3 = ["KursID*", "NazwaKursu"]
+ r3 = [["K10", "Bazy danych"], ["K20", "Algorytmy"], ["K30", "Optyka"]]
+ cw3 = [0.7, 1.1]
+ draw_table(ax, 4.8, 6.8, "Kursy", h3, r3, cw3, title_fontsize=9)
+
+ # ProwadzacyKurs (NEW - from BCNF decomposition)
+ h4 = ["Prowadzacy*", "KursID"]
+ r4 = [["Kowalski", "K10"], ["Nowak", "K20"], ["Wisniewski", "K30"]]
+ cw4 = [1.05, 0.7]
+ draw_table(
+ ax, 7.2, 6.8, "ProwadzacyKurs (kl: Prow.)", h4, r4, cw4, title_fontsize=9
+ )
+
+ # StudentProwadzacy (NEW)
+ h5 = ["StID*", "Prowadzacy*"]
+ r5 = [["1", "Kowalski"], ["1", "Nowak"], ["2", "Kowalski"], ["3", "Wisniewski"]]
+ cw5 = [0.55, 1.05]
+ draw_table(ax, 9.5, 6.8, "StudentProw. (kl: oba)", h5, r5, cw5, title_fontsize=9)
+
+ # Telefony
+ h6 = ["StID*", "Telefon*"]
+ r6 = [["1", "111-222"], ["1", "333-444"], ["2", "555-666"], ["3", "777-888"]]
+ cw6 = [0.55, 0.85]
+ draw_table(ax, 0.3, 4.6, "Telefony", h6, r6, cw6, title_fontsize=9)
+
+ # Annotations
+ add_label(
+ ax, 0.3, 2.9, "KROK: Zapisy(StID, KursID, Prowadzacy) rozbite na:", fontsize=9
+ )
+ add_label(
+ ax,
+ 0.3,
+ 2.55,
+ " ProwadzacyKurs(Prowadzacy, KursID) — FD: Prowadzacy -> KursID, klucz: Prowadzacy",
+ fontsize=8,
+ color="#333333",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 2.25,
+ " StudentProwadzacy(StID, Prowadzacy) — ktory student u ktorego prowadzacego",
+ fontsize=8,
+ color="#333333",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 1.85,
+ "Teraz KAZDA nietrywialna FD ma nadklucz po lewej stronie -> BCNF spelnione.",
+ fontsize=9,
+ )
+ add_label(
+ ax,
+ 0.3,
+ 1.45,
+ "Rekonstrukcja: StudentProw. JOIN ProwadzacyKurs ON Prowadzacy -> odtworzenie Zapisy.",
+ fontsize=8,
+ color="#333333",
+ )
+
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "nf_bcnf_tables.png"),
+ bbox_inches="tight",
+ facecolor="white",
+ pad_inches=0.2,
+ )
+ plt.close(fig)
+ print("Generated: nf_bcnf_tables.png")
+
+
+# ============================================================
+# DIAGRAM 6: 4NF example — multi-valued dependencies
+# ============================================================
+def draw_4nf() -> None:
+ """Draw 4nf."""
+ fig, ax = create_figure(11.69, 7.5)
+
+ # Before: table with MVD violation
+ h_before = ["StID*", "Hobby*", "Umiejetnosc*"]
+ r_before = [
+ ["1", "Szachy", "Python"],
+ ["1", "Szachy", "SQL"],
+ ["1", "Bieganie", "Python"],
+ ["1", "Bieganie", "SQL"],
+ ["2", "Plywanie", "Java"],
+ ]
+ cw_before = [0.55, 0.9, 1.0]
+ draw_table(
+ ax,
+ 0.5,
+ 6.8,
+ "PRZED: StudentAktywnosci (klucz: StID, Hobby, Umiejetnosc)",
+ h_before,
+ r_before,
+ cw_before,
+ highlight_cols={1, 2},
+ title_fontsize=10,
+ )
+
+ # Arrows
+ add_label(ax, 3.5, 6.3, "StID ->> Hobby", fontsize=9, color="black")
+ add_label(ax, 3.5, 6.0, "StID ->> Umiejetnosc", fontsize=9, color="black")
+ add_label(ax, 3.5, 5.6, "NIEZALEZNE MVD w jednej tabeli", fontsize=9, color="black")
+ add_label(
+ ax,
+ 3.5,
+ 5.2,
+ "= iloczyn kartezjanski = NARUSZENIE 4NF",
+ fontsize=9,
+ color="black",
+ )
+
+ # After: two tables
+ add_arrow(ax, 3.0, 4.2, 3.0, 3.7, "", "#333333")
+ add_label(ax, 3.2, 3.95, "dekompozycja", fontsize=8, color="#333333")
+
+ h_hobby = ["StID*", "Hobby*"]
+ r_hobby = [["1", "Szachy"], ["1", "Bieganie"], ["2", "Plywanie"]]
+ cw_hobby = [0.55, 0.9]
+ draw_table(
+ ax, 0.5, 3.5, "PO: StudentHobby", h_hobby, r_hobby, cw_hobby, title_fontsize=10
+ )
+
+ h_skill = ["StID*", "Umiejetnosc*"]
+ r_skill = [["1", "Python"], ["1", "SQL"], ["2", "Java"]]
+ cw_skill = [0.55, 1.0]
+ draw_table(
+ ax,
+ 3.5,
+ 3.5,
+ "PO: StudentUmiejetnosc",
+ h_skill,
+ r_skill,
+ cw_skill,
+ title_fontsize=10,
+ )
+
+ # Summary on the right side
+ add_label(ax, 6.5, 6.5, "4NF: BCNF + brak nietrywialnych MVD", fontsize=10)
+ add_label(
+ ax, 6.5, 6.1, "MVD X ->> Y: jeden X = ZBIOR Y-ow,", fontsize=8, color="#333333"
+ )
+ add_label(
+ ax, 6.5, 5.8, "niezaleznie od reszty kolumn.", fontsize=8, color="#333333"
+ )
+ add_label(
+ ax, 6.5, 5.35, "Naruszenie: Student 1 ma 2 hobby i 2 umiejetnosci", fontsize=8
+ )
+ add_label(
+ ax, 6.5, 5.05, " -> 2 x 2 = 4 wiersze (iloczyn kartezjanski!)", fontsize=8
+ )
+ add_label(
+ ax, 6.5, 4.65, "Naprawa: rozdziel niezalezne MVD do osobnych tabel.", fontsize=8
+ )
+ add_label(
+ ax,
+ 6.5,
+ 4.25,
+ "Po dekompozycji: 3 + 3 = 6 wierszy zamiast 5 z ilocz.",
+ fontsize=8,
+ color="#333333",
+ )
+ add_label(
+ ax, 6.5, 3.85, " (ale BEZ sztucznych kombinacji!)", fontsize=8, color="#333333"
+ )
+
+ # Key insight box
+ rect = mpatches.FancyBboxPatch(
+ (6.3, 2.5),
+ 5.0,
+ 1.0,
+ boxstyle="round,pad=0.1",
+ facecolor="#F0F0F0",
+ edgecolor="black",
+ linewidth=1.0,
+ )
+ ax.add_patch(rect)
+ add_label(ax, 6.5, 3.2, "ROZNICA 4NF vs BCNF:", fontsize=9)
+ add_label(
+ ax,
+ 6.5,
+ 2.85,
+ "BCNF dotyczy FD (X -> Y, jedna wartosc)",
+ fontsize=8,
+ color="#333333",
+ )
+ add_label(
+ ax,
+ 6.5,
+ 2.55,
+ "4NF dotyczy MVD (X ->> Y, zbior wartosci)",
+ fontsize=8,
+ color="#333333",
+ )
+
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "nf_4nf_example.png"),
+ bbox_inches="tight",
+ facecolor="white",
+ pad_inches=0.2,
+ )
+ plt.close(fig)
+ print("Generated: nf_4nf_example.png")
+
+
+# ============================================================
+# DIAGRAM 7: 5NF example — join dependencies
+# ============================================================
+def draw_5nf() -> None:
+ """Draw 5nf."""
+ fig, ax = create_figure(11.69, 8.5)
+
+ # Before: ternary table
+ h_before = ["Dostawca*", "Czesc*", "Projekt*"]
+ r_before = [
+ ["Alfa", "Sruba", "Most"],
+ ["Alfa", "Sruba", "Wiezowiec"],
+ ["Alfa", "Nakretka", "Most"],
+ ["Beta", "Sruba", "Wiezowiec"],
+ ["Beta", "Nakretka", "Wiezowiec"],
+ ]
+ cw_before = [0.9, 0.9, 1.0]
+ draw_table(
+ ax,
+ 0.5,
+ 7.8,
+ "PRZED: Dostawy (klucz: Dostawca, Czesc, Projekt)",
+ h_before,
+ r_before,
+ cw_before,
+ title_fontsize=10,
+ )
+
+ add_label(ax, 3.8, 7.3, "Tabela w 4NF (brak nietrywialnych MVD),", fontsize=8)
+ add_label(
+ ax, 3.8, 7.0, "ale NIE w 5NF jesli zachodzi regula cykliczna:", fontsize=8
+ )
+ add_label(
+ ax, 3.8, 6.55, "Jesli Dostawca dostarcza Czesc", fontsize=8, color="#333333"
+ )
+ add_label(
+ ax, 3.8, 6.25, " I Dostawca dostarcza do Projektu", fontsize=8, color="#333333"
+ )
+ add_label(
+ ax, 3.8, 5.95, " I Czesc jest uzywana w Projekcie", fontsize=8, color="#333333"
+ )
+ add_label(
+ ax,
+ 3.8,
+ 5.65,
+ " ==> Dostawca dostarcza te Czesc do tego Projektu.",
+ fontsize=8,
+ color="black",
+ )
+
+ # Arrow down
+ add_arrow(ax, 1.8, 5.1, 1.8, 4.6, "dekompozycja 5NF", "#333333")
+
+ # After: three binary tables
+ h1 = ["Dostawca*", "Czesc*"]
+ r1 = [
+ ["Alfa", "Sruba"],
+ ["Alfa", "Nakretka"],
+ ["Beta", "Sruba"],
+ ["Beta", "Nakretka"],
+ ]
+ cw1 = [0.9, 0.9]
+ draw_table(ax, 0.3, 4.3, "DostawcaCzesc", h1, r1, cw1, title_fontsize=9)
+
+ h2 = ["Dostawca*", "Projekt*"]
+ r2 = [["Alfa", "Most"], ["Alfa", "Wiezowiec"], ["Beta", "Wiezowiec"]]
+ cw2 = [0.9, 1.0]
+ draw_table(ax, 3.0, 4.3, "DostawcaProjekt", h2, r2, cw2, title_fontsize=9)
+
+ h3 = ["Czesc*", "Projekt*"]
+ r3 = [
+ ["Sruba", "Most"],
+ ["Sruba", "Wiezowiec"],
+ ["Nakretka", "Most"],
+ ["Nakretka", "Wiezowiec"],
+ ]
+ cw3 = [0.9, 1.0]
+ draw_table(ax, 5.7, 4.3, "CzescProjekt", h3, r3, cw3, title_fontsize=9)
+
+ # Join reconstruction note
+ rect = mpatches.FancyBboxPatch(
+ (8.3, 3.5),
+ 3.0,
+ 4.0,
+ boxstyle="round,pad=0.1",
+ facecolor="#F0F0F0",
+ edgecolor="black",
+ linewidth=1.0,
+ )
+ ax.add_patch(rect)
+
+ add_label(ax, 8.5, 7.2, "5NF (PJNF):", fontsize=10)
+ add_label(ax, 8.5, 6.8, "Project-Join NF", fontsize=8, color="#333333")
+ add_label(ax, 8.5, 6.35, "Kazda zaleznosc", fontsize=8)
+ add_label(ax, 8.5, 6.05, "zlaczenia (JD)", fontsize=8)
+ add_label(ax, 8.5, 5.75, "implikowana przez", fontsize=8)
+ add_label(ax, 8.5, 5.45, "klucze kandydujace.", fontsize=8)
+ add_label(ax, 8.5, 4.9, "Rekonstrukcja:", fontsize=9)
+ add_label(ax, 8.5, 4.55, "DC JOIN DP JOIN CP", fontsize=8, color="#333333")
+ add_label(ax, 8.5, 4.2, "= oryginalna tabela", fontsize=8, color="#333333")
+ add_label(ax, 8.5, 3.75, "(bezstratnie!)", fontsize=8, color="#333333")
+
+ # Verification example at the bottom
+ add_label(
+ ax,
+ 0.3,
+ 2.0,
+ "Weryfikacja: Alfa dostarcza Nakretke? Alfa -> Wiezowiec? Nakretka -> Wiezowiec?",
+ fontsize=8,
+ )
+ add_label(
+ ax,
+ 0.3,
+ 1.65,
+ " TAK, TAK, TAK --> wg reguly cyklicznej: Alfa dostarcza Nakretke do Wiezowca.",
+ fontsize=8,
+ color="#333333",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 1.25,
+ "Ale: Alfa dostarcza Nakretke? TAK. Alfa -> Most? TAK. Nakretka -> Most? TAK.",
+ fontsize=8,
+ )
+ add_label(
+ ax,
+ 0.3,
+ 0.9,
+ " --> Alfa dostarcza Nakretke do Mostu. (Tego wiersza NIE MA w oryginale -- BLAD!)",
+ fontsize=8,
+ color="black",
+ )
+ add_label(
+ ax,
+ 0.3,
+ 0.5,
+ " Dekompozycja 5NF jest poprawna TYLKO jesli regula cykliczna rzeczywiscie zachodzi!",
+ fontsize=8,
+ color="black",
+ )
+
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "nf_5nf_example.png"),
+ bbox_inches="tight",
+ facecolor="white",
+ pad_inches=0.2,
+ )
+ plt.close(fig)
+ print("Generated: nf_5nf_example.png")
+
+
+# ============================================================
+# DIAGRAM 8: Full normalization summary flowchart
+# ============================================================
+def draw_summary_flow() -> None:
+ """Draw summary flow."""
+ fig, ax = create_figure(11.69, 6.0)
+
+ # Boxes for each NF
+ box_y = 4.5
+ box_h = 1.8
+ box_w = 1.4
+ gap = 0.25
+
+ nf_data = [
+ ("0NF", "Nienormalna", "Listy w\nkomorkach,\nbrak klucza"),
+ ("1NF", "Atomowosc", "Kazda komorka\n= 1 wartosc,\njest klucz"),
+ ("2NF", "Pelny klucz", "Brak czesciowej\nzaleznosci od\nklucza zlozonego"),
+ ("3NF", "Tylko klucz", "Brak zaleznosci\nprzechodniej\nA->B->C"),
+ ("BCNF", "Nadklucz", "Lewa strona\nkazdej FD\n= nadklucz"),
+ ("4NF", "Brak MVD", "Brak nietryw.\nwielowart.\nzaleznosci"),
+ ("5NF", "Brak JD", "Kazda zal.\nzlaczenia\nimpl. kluczem"),
+ ]
+
+ for i, (name, subtitle, desc) in enumerate(nf_data):
+ x = 0.3 + i * (box_w + gap)
+
+ # Main box
+ rect = mpatches.FancyBboxPatch(
+ (x, box_y - box_h),
+ box_w,
+ box_h,
+ boxstyle="round,pad=0.05",
+ facecolor="#F5F5F5" if i == 0 else "#FFFFFF",
+ edgecolor="black",
+ linewidth=1.2,
+ )
+ ax.add_patch(rect)
+
+ # NF name
+ ax.text(
+ x + box_w / 2,
+ box_y - 0.15,
+ name,
+ fontsize=12,
+ fontweight="bold",
+ ha="center",
+ va="top",
+ family="monospace",
+ )
+
+ # Subtitle
+ ax.text(
+ x + box_w / 2,
+ box_y - 0.45,
+ subtitle,
+ fontsize=7,
+ ha="center",
+ va="top",
+ family="monospace",
+ color="#333333",
+ )
+
+ # Description
+ ax.text(
+ x + box_w / 2,
+ box_y - 0.75,
+ desc,
+ fontsize=6.5,
+ ha="center",
+ va="top",
+ family="monospace",
+ color="#555555",
+ linespacing=1.3,
+ )
+
+ # Arrow to next
+ if i < len(nf_data) - 1:
+ ax.annotate(
+ "",
+ xy=(x + box_w + 0.02, box_y - box_h / 2),
+ xytext=(x + box_w + gap - 0.02, box_y - box_h / 2),
+ arrowprops={"arrowstyle": "<-", "color": "black", "lw": 1.5},
+ )
+
+ # Bottom: mnemonic
+ ax.text(
+ 5.85,
+ 2.2,
+ '"Klucz, caly klucz i tylko klucz -- tak mi dopomoz Codd"',
+ fontsize=11,
+ ha="center",
+ va="center",
+ family="monospace",
+ style="italic",
+ )
+ ax.text(
+ 5.85,
+ 1.8,
+ "1NF: klucz istnieje | 2NF: caly klucz | 3NF: tylko klucz",
+ fontsize=9,
+ ha="center",
+ va="center",
+ family="monospace",
+ color="#333333",
+ )
+ ax.text(
+ 5.85,
+ 1.4,
+ "BCNF: kazdy determinant = nadklucz | 4NF: +brak MVD | 5NF: +brak JD",
+ fontsize=9,
+ ha="center",
+ va="center",
+ family="monospace",
+ color="#333333",
+ )
+
+ # Hierarchy
+ ax.text(
+ 5.85,
+ 0.8,
+ "5NF (zawiera sie w) 4NF (zaw.) BCNF (zaw.) 3NF (zaw.) 2NF (zaw.) 1NF",
+ fontsize=8,
+ ha="center",
+ va="center",
+ family="monospace",
+ color="#555555",
+ )
+
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "nf_summary_flow.png"),
+ bbox_inches="tight",
+ facecolor="white",
+ pad_inches=0.2,
+ )
+ plt.close(fig)
+ print("Generated: nf_summary_flow.png")
+
+
+# ============================================================
+# Main
+# ============================================================
+if __name__ == "__main__":
+ print("Generating normalization diagrams...")
+ draw_0nf()
+ draw_1nf()
+ draw_2nf()
+ draw_3nf()
+ draw_bcnf()
+ draw_4nf()
+ draw_5nf()
+ draw_summary_flow()
+ print("\nDone! All diagrams saved to:", OUTPUT_DIR)
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_pattern_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_pattern_diagrams.py
new file mode 100755
index 0000000..d3cd02a
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_pattern_diagrams.py
@@ -0,0 +1,952 @@
+#!/usr/bin/env python3
+"""Generate pattern cataloguing diagrams for PYTANIE 14 (AIS).
+
+ 1. Pattern Template Structure — the standard fields every pattern has
+ 2. Catalog Classification Map — catalogs arranged by scope & domain
+ 3. Pattern Language Network — how patterns reference each other.
+
+All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+import numpy as np
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 9
+FS_TITLE = 13
+FS_SMALL = 7.5
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.2,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=True,
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y), w, h, boxstyle="round,pad=0.08", lw=lw, edgecolor=LN, facecolor=fill
+ )
+ else:
+ rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ )
+
+
+# ============================================================
+# 1. Pattern Template Structure (NaPSiRoKo mnemonic)
+# ============================================================
+def generate_pattern_template() -> None:
+ """Generate pattern template."""
+ fig, ax = plt.subplots(figsize=(8.27, 6))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 8)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG)
+ ax.set_title(
+ "Szablon opisu wzorca \u2014 \u201eNaPSiRoKo\u201d",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=15,
+ )
+
+ # Main card outline
+ card_x, card_y, card_w, card_h = 1.5, 0.5, 7, 7
+ card = FancyBboxPatch(
+ (card_x, card_y),
+ card_w,
+ card_h,
+ boxstyle="round,pad=0.15",
+ lw=2.5,
+ edgecolor=LN,
+ facecolor=GRAY4,
+ )
+ ax.add_patch(card)
+
+ # Title of card
+ ax.text(
+ card_x + card_w / 2,
+ card_y + card_h - 0.35,
+ "KARTA WZORCA",
+ ha="center",
+ va="center",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ )
+
+ # Fields as horizontal bands
+ fields = [
+ ("Na", "NAZWA", "Layered, Observer, Microservices", GRAY1),
+ (
+ "P",
+ "PROBLEM / KONTEKST",
+ "Kiedy stosować? Jaki problem rozwiązuje?",
+ "white",
+ ),
+ (
+ "Si",
+ "SIŁY (forces)",
+ "Konkurencyjne wymagania do pogodzenia\n(np. testowalność vs wydajność)",
+ GRAY1,
+ ),
+ ("Ro", "ROZWIĄZANIE", "Struktura, diagram, zachowanie", "white"),
+ ("Ko", "KONSEKWENCJE", "Tradeoffs: co zyskujemy, co tracimy", GRAY1),
+ ]
+
+ band_x = card_x + 0.3
+ band_w = card_w - 0.6
+ band_h = 1.05
+ start_y = card_y + card_h - 1.1
+
+ for i, (abbr, title, desc, fill) in enumerate(fields):
+ by = start_y - i * (band_h + 0.15)
+
+ # Abbreviation circle on the left
+ circle = plt.Circle(
+ (band_x + 0.35, by + band_h / 2),
+ 0.28,
+ lw=1.5,
+ edgecolor=LN,
+ facecolor=GRAY2,
+ )
+ ax.add_patch(circle)
+ ax.text(
+ band_x + 0.35,
+ by + band_h / 2,
+ abbr,
+ ha="center",
+ va="center",
+ fontsize=10,
+ fontweight="bold",
+ )
+
+ # Field box
+ fx = band_x + 0.8
+ fw = band_w - 0.8
+ rect = FancyBboxPatch(
+ (fx, by),
+ fw,
+ band_h,
+ boxstyle="round,pad=0.06",
+ lw=1,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ fx + 0.15,
+ by + band_h - 0.25,
+ title,
+ ha="left",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.text(
+ fx + 0.15,
+ by + 0.25,
+ desc,
+ ha="left",
+ va="center",
+ fontsize=FS_SMALL,
+ fontstyle="italic",
+ color="#444444",
+ )
+
+ # Arrow connecting fields
+ if i < len(fields) - 1:
+ draw_arrow(ax, band_x + 0.35, by - 0.02, band_x + 0.35, by - 0.13, lw=1.0)
+
+ # Extra fields note at bottom
+ ax.text(
+ card_x + card_w / 2,
+ card_y + 0.25,
+ "+ Powiązane wzorce • Znane zastosowania • Warianty",
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ fontstyle="italic",
+ )
+
+ # Mnemonic reminder on the right
+ ax.text(
+ 9.8,
+ 4,
+ "Mnemonik:\nNaPSiRoKo",
+ ha="center",
+ va="center",
+ fontsize=10,
+ fontweight="bold",
+ rotation=90,
+ color="#666666",
+ )
+
+ fig.tight_layout()
+ out = str(Path(OUTPUT_DIR) / "q14_pattern_template.png")
+ fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" Saved: {out}")
+
+
+# ============================================================
+# 2. Catalog Classification Map
+# ============================================================
+def generate_catalog_map() -> None:
+ """Generate catalog map."""
+ fig, ax = plt.subplots(figsize=(8.27, 7))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 9)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG)
+ ax.set_title(
+ "Mapa katalog\u00f3w wzorc\u00f3w \u2014 \u201ePawe\u0142 Gra\u0142 Efektownie Pod Chmurami\u201d",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=15,
+ )
+
+ # Y-axis: Scale (architectural → design → idiom)
+ ax.text(
+ 0.3,
+ 7.8,
+ "SKALA",
+ fontsize=10,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ rotation=90,
+ )
+ ax.annotate(
+ "",
+ xy=(0.3, 2.0),
+ xytext=(0.3, 7.5),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+
+ scale_labels = [
+ (7.0, "Architektoniczny\n(cały system)"),
+ (5.0, "Projektowy\n(klasa/obiekt)"),
+ (3.0, "Idiomatyczny\n(linia kodu)"),
+ ]
+ for sy, label in scale_labels:
+ ax.text(
+ 1.0,
+ sy,
+ label,
+ fontsize=FS_SMALL,
+ ha="left",
+ va="center",
+ fontstyle="italic",
+ )
+ ax.plot([0.15, 0.45], [sy, sy], color=GRAY3, lw=0.8, ls="--")
+
+ # X-axis: Domain
+ ax.text(
+ 6.5,
+ 1.2,
+ "DOMENA ZASTOSOWANIA",
+ fontsize=10,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ )
+ ax.annotate(
+ "",
+ xy=(11.5, 1.5),
+ xytext=(2.0, 1.5),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+
+ # Catalog boxes positioned by scale x domain
+ catalogs = [
+ # (x, y, w, h, name, subtitle, fill, mnemonic_letter)
+ (
+ 2.5,
+ 6.2,
+ 2.5,
+ 1.4,
+ "POSA",
+ "1996 • Buschmann\nLayers, Broker,\nPipes & Filters, MVC",
+ GRAY1,
+ "P",
+ ),
+ (
+ 2.5,
+ 4.2,
+ 2.5,
+ 1.4,
+ "GoF",
+ "1994 • Gamma et al.\n23 wzorce:\n5 kreac. / 7 strukt. / 11 behaw.",
+ GRAY2,
+ "G",
+ ),
+ (
+ 5.5,
+ 6.2,
+ 2.5,
+ 1.4,
+ "EIP",
+ "2003 • Hohpe & Woolf\nMessage Channel,\nRouter, Aggregator",
+ GRAY1,
+ "E",
+ ),
+ (
+ 5.5,
+ 4.2,
+ 2.5,
+ 1.4,
+ "PoEAA",
+ "2002 • M. Fowler\nRepository, Unit of Work,\nDomain Model",
+ "white",
+ "P",
+ ),
+ (
+ 8.5,
+ 6.2,
+ 2.8,
+ 1.4,
+ "Cloud\nPatterns",
+ "~2015 • Azure/AWS\nCircuit Breaker,\nSaga, Sidecar",
+ GRAY1,
+ "C",
+ ),
+ ]
+
+ for cx, cy, cw, ch, name, sub, fill, ml in catalogs:
+ rect = FancyBboxPatch(
+ (cx, cy),
+ cw,
+ ch,
+ boxstyle="round,pad=0.1",
+ lw=1.5,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ cx + cw / 2,
+ cy + ch - 0.3,
+ name,
+ ha="center",
+ va="center",
+ fontsize=10,
+ fontweight="bold",
+ )
+ ax.text(
+ cx + cw / 2,
+ cy + 0.4,
+ sub,
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ linespacing=1.3,
+ )
+
+ # Mnemonic letter in corner
+ circle = plt.Circle(
+ (cx + 0.25, cy + ch - 0.25), 0.2, lw=1, edgecolor=LN, facecolor=GRAY5
+ )
+ ax.add_patch(circle)
+ ax.text(
+ cx + 0.25,
+ cy + ch - 0.25,
+ ml,
+ ha="center",
+ va="center",
+ fontsize=8,
+ fontweight="bold",
+ )
+
+ # Mnemonic bar at bottom
+ mnem_y = 2.2
+ ax.text(
+ 6.0,
+ mnem_y,
+ "PGEP+C → Paweł Grał Efektownie Pod Chmurami",
+ ha="center",
+ va="center",
+ fontsize=10,
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": LN,
+ "lw": 1.5,
+ },
+ )
+
+ # Domain labels along x-axis
+ domains = [
+ (3.75, 1.7, "Architektura"),
+ (6.75, 1.7, "Integracja / Enterprise"),
+ (9.9, 1.7, "Chmura"),
+ ]
+ for dx, dy, dlabel in domains:
+ ax.text(
+ dx,
+ dy,
+ dlabel,
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ fontstyle="italic",
+ )
+
+ fig.tight_layout()
+ out = str(Path(OUTPUT_DIR) / "q14_catalog_map.png")
+ fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" Saved: {out}")
+
+
+# ============================================================
+# 3. Three Pillars of Cataloguing
+# ============================================================
+def generate_three_pillars() -> None:
+ """Generate three pillars."""
+ fig, ax = plt.subplots(figsize=(8.27, 5.5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG)
+ ax.set_title(
+ "Jak są katalogowane wzorce? — Trzy filary",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=15,
+ )
+
+ # Roof / banner
+ roof_pts = np.array([[1, 5.5], [6, 6.8], [11, 5.5]])
+ roof = plt.Polygon(roof_pts, closed=True, lw=2, edgecolor=LN, facecolor=GRAY4)
+ ax.add_patch(roof)
+ ax.text(
+ 6,
+ 6.0,
+ "KATALOGOWANIE WZORCÓW",
+ ha="center",
+ va="center",
+ fontsize=11,
+ fontweight="bold",
+ )
+
+ # Three pillars
+ pillars = [
+ (
+ 1.3,
+ "1. SZABLON\nOPISU",
+ "Każdy wzorzec ma\nte same pola:\nNazwa → Problem\n→ Siły → Rozwiązanie\n→ Konsekwencje",
+ "Analogia:\nformatka\nencyklopedii",
+ ),
+ (
+ 4.8,
+ "2. KLASYFIKACJA\nWIELOOSIOWA",
+ "Osie podziału:\n• Skala (arch/proj/idiom)\n• Domena problemu\n• Atrybut jakościowy\n• Domena zastosowania",
+ "Analogia:\nkategorie\nw bibliotece",
+ ),
+ (
+ 8.3,
+ "3. JĘZYK\nWZORCÓW",
+ "Wzorce referują się\nwzajemnie tworząc\nsieć/graf:\nA → wymaga → B\nB → wariant → C",
+ "Analogia:\n\u201ezobacz te\u017c\u201d\nw encyklopedii",
+ ),
+ ]
+
+ for px, title, desc, analogy in pillars:
+ pw, ph = 2.8, 5.0
+ py = 0.5
+
+ # Pillar rectangle
+ rect = FancyBboxPatch(
+ (px, py),
+ pw,
+ ph,
+ boxstyle="round,pad=0.1",
+ lw=1.8,
+ edgecolor=LN,
+ facecolor="white",
+ )
+ ax.add_patch(rect)
+
+ # Title
+ ax.text(
+ px + pw / 2,
+ py + ph - 0.55,
+ title,
+ ha="center",
+ va="center",
+ fontsize=9,
+ fontweight="bold",
+ )
+
+ # Horizontal line under title
+ ax.plot(
+ [px + 0.2, px + pw - 0.2], [py + ph - 1.0, py + ph - 1.0], color=LN, lw=0.8
+ )
+
+ # Description
+ ax.text(
+ px + pw / 2,
+ py + ph / 2 - 0.3,
+ desc,
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ linespacing=1.4,
+ )
+
+ # Analogy box at bottom
+ analogy_rect = FancyBboxPatch(
+ (px + 0.2, py + 0.15),
+ pw - 0.4,
+ 1.0,
+ boxstyle="round,pad=0.06",
+ lw=0.8,
+ edgecolor=GRAY3,
+ facecolor=GRAY1,
+ )
+ ax.add_patch(analogy_rect)
+ ax.text(
+ px + pw / 2,
+ py + 0.65,
+ analogy,
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ fontstyle="italic",
+ color="#555555",
+ )
+
+ fig.tight_layout()
+ out = str(Path(OUTPUT_DIR) / "q14_three_pillars.png")
+ fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" Saved: {out}")
+
+
+# ============================================================
+# 4. Filled-in Observer Pattern Card
+# ============================================================
+def generate_observer_card_filled() -> None:
+ """Generate observer card filled."""
+ fig, ax = plt.subplots(figsize=(8.27, 8.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG)
+ ax.set_title(
+ "Wypełniona karta wzorca — Observer (GoF)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=15,
+ )
+
+ # Main card outline
+ card_x, card_y, card_w, card_h = 0.8, 0.3, 8.4, 9.2
+ card = FancyBboxPatch(
+ (card_x, card_y),
+ card_w,
+ card_h,
+ boxstyle="round,pad=0.15",
+ lw=2.5,
+ edgecolor=LN,
+ facecolor=GRAY4,
+ )
+ ax.add_patch(card)
+
+ # Fields with actual Observer content
+ fields = [
+ ("Na", "NAZWA", "Observer", GRAY2, True),
+ (
+ "P",
+ "PROBLEM",
+ "Obiekt (Subject) zmienia stan → wielu zależnych\n"
+ "obiektów musi zareagować, ale Subject nie\n"
+ "powinien znać ich konkretnych typów.",
+ GRAY1,
+ False,
+ ),
+ (
+ "Si",
+ "SIŁY",
+ "• loose coupling (nie znać obserwatorów z nazwy)\n"
+ " vs koszt powiadomień (N obserwatorów = N wywołań)\n"
+ "• otwartość na rozszerzenia vs złożoność debugowania",
+ "white",
+ False,
+ ),
+ (
+ "Ro",
+ "ROZWIĄZANIE",
+ "Subject przechowuje listę Observer.\n"
+ "Metody: attach(o), detach(o), notify().\n"
+ "notify() iteruje po liście i woła update()\n"
+ "na każdym obserwatorze.",
+ GRAY1,
+ False,
+ ),
+ (
+ "Ko",
+ "KONSEKWENCJE",
+ "(+) Luźne wiązanie — Subject ↔ Observer\n"
+ "(+) Nowi obserwatorzy bez zmian w Subject\n"
+ "(-) Kaskada powiadomień może być kosztowna\n"
+ "(-) Memory leaks jeśli nie detach()",
+ "white",
+ False,
+ ),
+ ]
+
+ band_x = card_x + 0.3
+ band_w = card_w - 0.6
+ start_y = card_y + card_h - 0.65
+
+ for i, (abbr, title, content, fill, is_title_field) in enumerate(fields):
+ if is_title_field:
+ band_h = 0.7
+ elif i == 1:
+ band_h = 1.3
+ elif i == 2:
+ band_h = 1.4
+ elif i == 3:
+ band_h = 1.5
+ else:
+ band_h = 1.5
+
+ by = start_y - sum(
+ (0.7 if j == 0 else 1.3 if j == 1 else 1.4 if j == 2 else 1.5) + 0.15
+ for j in range(i)
+ )
+
+ # Abbreviation circle
+ circle = plt.Circle(
+ (band_x + 0.35, by + band_h / 2),
+ 0.28,
+ lw=1.5,
+ edgecolor=LN,
+ facecolor=GRAY3,
+ )
+ ax.add_patch(circle)
+ ax.text(
+ band_x + 0.35,
+ by + band_h / 2,
+ abbr,
+ ha="center",
+ va="center",
+ fontsize=10,
+ fontweight="bold",
+ )
+
+ # Field box
+ fx = band_x + 0.8
+ fw = band_w - 0.8
+ rect = FancyBboxPatch(
+ (fx, by),
+ fw,
+ band_h,
+ boxstyle="round,pad=0.06",
+ lw=1,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+
+ if is_title_field:
+ ax.text(
+ fx + fw / 2,
+ by + band_h / 2,
+ f"{title}: {content}",
+ ha="center",
+ va="center",
+ fontsize=12,
+ fontweight="bold",
+ )
+ else:
+ ax.text(
+ fx + 0.15,
+ by + band_h - 0.2,
+ title,
+ ha="left",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.text(
+ fx + 0.15,
+ by + band_h / 2 - 0.15,
+ content,
+ ha="left",
+ va="center",
+ fontsize=FS_SMALL,
+ family="monospace",
+ linespacing=1.3,
+ )
+
+ # Arrow
+ if i < len(fields) - 1:
+ draw_arrow(ax, band_x + 0.35, by - 0.02, band_x + 0.35, by - 0.13, lw=1.0)
+
+ # Extra info at bottom
+ extra_y = 0.55
+ extras = [
+ "Powiązane: Mediator (centralizuje), Pub/Sub (rozproszony), MVC (View = Observer)",
+ "Znane użycia: Java Swing listeners, C# event/delegate, React useState, DOM addEventListener",
+ ]
+ for j, txt in enumerate(extras):
+ ax.text(
+ card_x + card_w / 2,
+ extra_y + (1 - j) * 0.25,
+ txt,
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ fontstyle="italic",
+ color="#444444",
+ )
+
+ fig.tight_layout()
+ out = str(Path(OUTPUT_DIR) / "q14_observer_card_filled.png")
+ fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" Saved: {out}")
+
+
+# ============================================================
+# 5. Pattern Language Navigation Graph
+# ============================================================
+def generate_pattern_language_navigation() -> None:
+ """Generate pattern language navigation."""
+ fig, ax = plt.subplots(figsize=(8.27, 9))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 12)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG)
+ ax.set_title(
+ 'Język wzorców — nawigacja „problem → wzorzec → nowy problem"',
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=15,
+ )
+
+ # Node positions: (x, y, label, is_pattern, fill)
+ # Left column: problems; Right column: patterns
+ nodes = [
+ # Problems (left, rounded rect, white)
+ (1.5, 10.5, "Monolith\nnie skaluje się", False, "white"),
+ (1.5, 8.2, "Jak routować\nżądania do\nserwisów?", False, "white"),
+ (1.5, 5.9, "Co gdy serwis\nnie odpowiada?", False, "white"),
+ (1.5, 3.6, "Jak zachować\nspójność\ntransakcji?", False, "white"),
+ (1.5, 1.3, "Jak odnaleźć\nadres serwisu?", False, "white"),
+ # Patterns (right, filled rect, gray)
+ (7.0, 9.3, "Microservices", True, GRAY2),
+ (7.0, 7.0, "API Gateway", True, GRAY2),
+ (7.0, 4.7, "Circuit Breaker", True, GRAY2),
+ (7.0, 2.4, "Saga", True, GRAY2),
+ (10.0, 5.9, "Service\nDiscovery", True, GRAY1),
+ ]
+
+ # Draw nodes
+ node_w_prob = 2.8
+ node_h_prob = 1.3
+ node_w_pat = 2.5
+ node_h_pat = 1.0
+
+ for nx, ny, label, is_pattern, fill in nodes:
+ if is_pattern:
+ w, h = node_w_pat, node_h_pat
+ rect = FancyBboxPatch(
+ (nx - w / 2, ny - h / 2),
+ w,
+ h,
+ boxstyle="round,pad=0.1",
+ lw=2,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ nx, ny, label, ha="center", va="center", fontsize=10, fontweight="bold"
+ )
+ else:
+ w, h = node_w_prob, node_h_prob
+ rect = FancyBboxPatch(
+ (nx - w / 2, ny - h / 2),
+ w,
+ h,
+ boxstyle="round,pad=0.1",
+ lw=1.2,
+ edgecolor=LN,
+ facecolor=fill,
+ linestyle="--",
+ )
+ ax.add_patch(rect)
+ ax.text(
+ nx,
+ ny,
+ label,
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ fontstyle="italic",
+ )
+
+ # Arrows: problem → pattern (solid), pattern → problem (dashed label)
+ arrows = [
+ # (x1, y1, x2, y2, label, style)
+ (2.9, 10.5, 5.75, 9.5, "rozwiązuje →", "->", 1.5),
+ (7.0, 8.8, 2.9, 8.5, "← rodzi problem", "->", 1.0),
+ (2.9, 8.0, 5.75, 7.2, "rozwiązuje →", "->", 1.5),
+ (7.0, 6.5, 2.9, 6.2, "← rodzi problem", "->", 1.0),
+ (2.9, 5.7, 5.75, 5.0, "rozwiązuje →", "->", 1.5),
+ (7.0, 4.2, 2.9, 3.9, "← rodzi problem", "->", 1.0),
+ (2.9, 3.3, 5.75, 2.6, "rozwiązuje →", "->", 1.5),
+ # Microservices → Service Discovery
+ (8.25, 9.0, 9.5, 6.5, "wymaga →", "->", 1.0),
+ # Problem → Service Discovery
+ (2.9, 1.3, 8.75, 5.6, "rozwiązuje →", "->", 1.2),
+ ]
+
+ for x1, y1, x2, y2, label, style, lw in arrows:
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={
+ "arrowstyle": style,
+ "color": LN,
+ "lw": lw,
+ "connectionstyle": "arc3,rad=0.05",
+ },
+ )
+ mx, my = (x1 + x2) / 2, (y1 + y2) / 2
+ ax.text(
+ mx,
+ my + 0.2,
+ label,
+ ha="center",
+ va="center",
+ fontsize=6.5,
+ fontstyle="italic",
+ color="#555555",
+ bbox={
+ "boxstyle": "round,pad=0.1",
+ "facecolor": "white",
+ "edgecolor": "none",
+ "alpha": 0.8,
+ },
+ )
+
+ # Legend
+ legend_y = 0.3
+ # Problem node
+ r1 = FancyBboxPatch(
+ (1.0, legend_y - 0.2),
+ 1.5,
+ 0.4,
+ boxstyle="round,pad=0.05",
+ lw=1,
+ edgecolor=LN,
+ facecolor="white",
+ linestyle="--",
+ )
+ ax.add_patch(r1)
+ ax.text(1.75, legend_y, "Problem", ha="center", va="center", fontsize=7)
+ # Pattern node
+ r2 = FancyBboxPatch(
+ (3.5, legend_y - 0.2),
+ 1.5,
+ 0.4,
+ boxstyle="round,pad=0.05",
+ lw=1.5,
+ edgecolor=LN,
+ facecolor=GRAY2,
+ )
+ ax.add_patch(r2)
+ ax.text(
+ 4.25,
+ legend_y,
+ "Wzorzec",
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ )
+ ax.text(
+ 6.5,
+ legend_y,
+ "Nawigacja: Problem → Wzorzec → Nowy Problem → Wzorzec → ...",
+ ha="left",
+ va="center",
+ fontsize=7,
+ fontstyle="italic",
+ )
+
+ fig.tight_layout()
+ out = str(Path(OUTPUT_DIR) / "q14_pattern_language_navigation.png")
+ fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" Saved: {out}")
+
+
+# ============================================================
+# Main
+# ============================================================
+if __name__ == "__main__":
+ print("Generating PYTANIE 14 diagrams...")
+ generate_pattern_template()
+ generate_catalog_map()
+ generate_three_pillars()
+ generate_observer_card_filled()
+ generate_pattern_language_navigation()
+ print("Done!")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_process_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_process_diagrams.py
new file mode 100755
index 0000000..5ef356c
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_process_diagrams.py
@@ -0,0 +1,708 @@
+#!/usr/bin/env python3
+"""Generate 4 process modeling diagrams (BPMN, UML Activity, EPC, Flowchart).
+
+all representing the same process: "Obsluga reklamacji" (Complaint Handling).
+Output: A4-compatible, black & white, laser-printer-friendly PNG files.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch, Polygon
+from matplotlib.path import Path as MplPath
+import matplotlib.pyplot as plt
+
+# --- Common settings ---
+DPI = 300
+BG_COLOR = "white"
+LINE_COLOR = "black"
+FONT_SIZE = 9
+TITLE_SIZE = 14
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+
+def draw_arrow(ax, x1, y1, x2, y2) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": "->", "color": LINE_COLOR, "lw": 1.3},
+ )
+
+
+def draw_line(ax, x1, y1, x2, y2) -> None:
+ """Draw line."""
+ ax.plot([x1, x2], [y1, y2], color=LINE_COLOR, lw=1.3, solid_capstyle="round")
+
+
+def draw_rounded_rect(
+ ax, x, y, w, h, text, fill="white", lw=1.5, fontsize=FONT_SIZE
+) -> None:
+ """Draw rounded rect."""
+ rect = FancyBboxPatch(
+ (x - w / 2, y - h / 2),
+ w,
+ h,
+ boxstyle="round,pad=0.3",
+ linewidth=lw,
+ edgecolor=LINE_COLOR,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(x, y, text, ha="center", va="center", fontsize=fontsize)
+
+
+def draw_diamond(ax, x, y, size, text="", fill="white", fontsize=8) -> None:
+ """Draw diamond."""
+ s = size
+ diamond = Polygon(
+ [(x, y + s), (x + s, y), (x, y - s), (x - s, y)],
+ closed=True,
+ linewidth=1.5,
+ edgecolor=LINE_COLOR,
+ facecolor=fill,
+ )
+ ax.add_patch(diamond)
+ if text:
+ ax.text(
+ x, y, text, ha="center", va="center", fontsize=fontsize, fontweight="bold"
+ )
+
+
+# =========================================================================
+# 1. BPMN 2.0 Diagram
+# =========================================================================
+def generate_bpmn() -> None:
+ """Generate bpmn."""
+ fig, ax = plt.subplots(figsize=(11, 7.5))
+ ax.set_xlim(0, 110)
+ ax.set_ylim(0, 75)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG_COLOR)
+ ax.set_title(
+ "BPMN 2.0 \u2014 Obs\u0142uga reklamacji",
+ fontsize=TITLE_SIZE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # --- Pool outline ---
+ pool_x, pool_y, pool_w, pool_h = 3, 3, 104, 68
+ ax.add_patch(
+ plt.Rectangle(
+ (pool_x, pool_y),
+ pool_w,
+ pool_h,
+ lw=2,
+ edgecolor=LINE_COLOR,
+ facecolor="white",
+ )
+ )
+
+ # Pool label
+ label_strip = pool_x + 4
+ ax.plot(
+ [label_strip, label_strip], [pool_y, pool_y + pool_h], color=LINE_COLOR, lw=1.5
+ )
+ ax.text(
+ pool_x + 2,
+ pool_y + pool_h / 2,
+ "FIRMA",
+ fontsize=11,
+ fontweight="bold",
+ rotation=90,
+ ha="center",
+ va="center",
+ )
+
+ # --- Lanes ---
+ lane_top = pool_y + pool_h
+ lane_mid1 = pool_y + pool_h * 2 / 3
+ lane_mid2 = pool_y + pool_h * 1 / 3
+
+ ax.plot(
+ [label_strip, pool_x + pool_w], [lane_mid1, lane_mid1], color=LINE_COLOR, lw=1
+ )
+ ax.plot(
+ [label_strip, pool_x + pool_w], [lane_mid2, lane_mid2], color=LINE_COLOR, lw=1
+ )
+
+ y_bok = (lane_top + lane_mid1) / 2
+ y_jak = (lane_mid1 + lane_mid2) / 2
+ y_mag = (lane_mid2 + pool_y) / 2
+
+ ax.text(
+ label_strip + 2.5,
+ y_bok,
+ "BOK",
+ fontsize=8,
+ ha="center",
+ va="center",
+ rotation=90,
+ fontstyle="italic",
+ )
+ ax.text(
+ label_strip + 2.5,
+ y_jak,
+ "Jako\u015b\u0107",
+ fontsize=8,
+ ha="center",
+ va="center",
+ rotation=90,
+ fontstyle="italic",
+ )
+ ax.text(
+ label_strip + 2.5,
+ y_mag,
+ "Magazyn",
+ fontsize=8,
+ ha="center",
+ va="center",
+ rotation=90,
+ fontstyle="italic",
+ )
+
+ content_left = label_strip + 5
+
+ # --- Elements ---
+ # Start event
+ sx = content_left + 4
+ ax.add_patch(
+ plt.Circle((sx, y_bok), 2, lw=2, edgecolor=LINE_COLOR, facecolor="white")
+ )
+ ax.text(sx, y_bok - 3.5, "Reklamacja\nwp\u0142ywa", fontsize=6, ha="center")
+
+ # Task 1: Przyjmij zg\u0142oszenie (BOK)
+ t1x = sx + 14
+ draw_rounded_rect(ax, t1x, y_bok, 14, 6, "Przyjmij\nzg\u0142oszenie")
+ draw_arrow(ax, sx + 2, y_bok, t1x - 7, y_bok)
+
+ # Task 2: Zweryfikuj zasadno\u015b\u0107 (Jako\u015b\u0107) \u2014 elbow routing
+ t2x = t1x + 18
+ draw_rounded_rect(ax, t2x, y_jak, 14, 6, "Zweryfikuj\nzasadno\u015b\u0107")
+ elbow_x = t1x + 10
+ draw_line(ax, t1x + 7, y_bok, elbow_x, y_bok)
+ draw_line(ax, elbow_x, y_bok, elbow_x, y_jak)
+ draw_arrow(ax, elbow_x, y_jak, t2x - 7, y_jak)
+
+ # XOR Gateway (split)
+ gx = t2x + 14
+ draw_diamond(ax, gx, y_jak, 3.5, "X")
+ draw_arrow(ax, t2x + 7, y_jak, gx - 3.5, y_jak)
+
+ # YES: down to Magazyn
+ t3x = gx + 14
+ draw_rounded_rect(ax, t3x, y_mag, 14, 6, "Przygotuj\nwymian\u0119/zwrot")
+ draw_line(ax, gx, y_jak - 3.5, gx, y_mag)
+ draw_arrow(ax, gx, y_mag, t3x - 7, y_mag)
+ ax.text(gx + 1.5, y_jak - 6, "Tak", fontsize=7, ha="left")
+
+ # NO: right in Jako\u015b\u0107
+ t4x = gx + 14
+ draw_rounded_rect(ax, t4x, y_jak, 14, 6, "Odrzu\u0107\nreklamacj\u0119")
+ draw_arrow(ax, gx + 3.5, y_jak, t4x - 7, y_jak)
+ ax.text(gx + 4, y_jak + 2, "Nie", fontsize=7, ha="left")
+
+ # XOR merge (in BOK)
+ mx = t4x + 14
+ draw_diamond(ax, mx, y_bok, 3.5, "X")
+ # From Odrzu\u0107 up to merge
+ draw_line(ax, t4x + 7, y_jak, mx, y_jak)
+ draw_arrow(ax, mx, y_jak, mx, y_bok - 3.5)
+ # From Przygotuj wymian\u0119 up to merge (offset to avoid overlap)
+ draw_line(ax, t3x + 7, y_mag, mx - 4, y_mag)
+ draw_line(ax, mx - 4, y_mag, mx - 4, y_bok)
+ draw_arrow(ax, mx - 4, y_bok, mx - 3.5, y_bok)
+
+ # Task 5: Powiadom klienta (BOK)
+ t5x = mx + 13
+ draw_rounded_rect(ax, t5x, y_bok, 14, 6, "Powiadom\nklienta")
+ draw_arrow(ax, mx + 3.5, y_bok, t5x - 7, y_bok)
+
+ # End event
+ ex = t5x + 12
+ ax.add_patch(
+ plt.Circle((ex, y_bok), 2, lw=3, edgecolor=LINE_COLOR, facecolor="white")
+ )
+ draw_arrow(ax, t5x + 7, y_bok, ex - 2, y_bok)
+ ax.text(ex, y_bok - 3.5, "Koniec", fontsize=6, ha="center")
+
+ # --- Legend ---
+ ly = 1
+ ax.text(12, ly, "Legenda:", fontsize=7, fontweight="bold", va="center")
+ ax.add_patch(plt.Circle((22, ly), 1, lw=2, edgecolor=LINE_COLOR, facecolor="white"))
+ ax.text(24, ly, "Start", fontsize=6, va="center")
+ ax.add_patch(plt.Circle((30, ly), 1, lw=3, edgecolor=LINE_COLOR, facecolor="white"))
+ ax.text(32, ly, "Koniec", fontsize=6, va="center")
+ draw_diamond(ax, 40, ly, 1.5, "X", fontsize=5)
+ ax.text(43, ly, "Bramka XOR", fontsize=6, va="center")
+ draw_rounded_rect(ax, 58, ly, 7, 2.5, "Zadanie", fontsize=6)
+ ax.text(65, ly, "Sequence Flow \u2192", fontsize=6, va="center")
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "bpmn_reklamacja.png"),
+ dpi=DPI,
+ facecolor="white",
+ bbox_inches="tight",
+ )
+ plt.close(fig)
+ print(" OK BPMN saved")
+
+
+# =========================================================================
+# 2. UML Activity Diagram
+# =========================================================================
+def generate_uml_activity() -> None:
+ """Generate uml activity."""
+ fig, ax = plt.subplots(figsize=(8.27, 10))
+ ax.set_xlim(0, 100)
+ ax.set_ylim(0, 100)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG_COLOR)
+ ax.set_title(
+ "UML Activity Diagram \u2014 Obs\u0142uga reklamacji",
+ fontsize=TITLE_SIZE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ cx = 50
+ y = 93
+ step = 11
+
+ # Initial node
+ ax.add_patch(plt.Circle((cx, y), 1.8, facecolor="black", edgecolor="black"))
+
+ # Action 1
+ y -= step
+ draw_rounded_rect(ax, cx, y, 28, 6, "Przyjmij zg\u0142oszenie reklamacji")
+ draw_arrow(ax, cx, y + step - 1.8, cx, y + 3)
+
+ # Action 2
+ y -= step
+ draw_rounded_rect(ax, cx, y, 28, 6, "Zweryfikuj zasadno\u015b\u0107")
+ draw_arrow(ax, cx, y + step - 3, cx, y + 3)
+
+ # Decision
+ y -= step
+ draw_diamond(ax, cx, y, 4)
+ draw_arrow(ax, cx, y + step - 3, cx, y + 4)
+ ax.text(cx + 6, y + 5, "[zasadna?]", fontsize=8, fontstyle="italic")
+
+ dec_y = y
+ branch_y = dec_y - step
+
+ # Left [tak]
+ left_x = cx - 24
+ draw_rounded_rect(ax, left_x, branch_y, 22, 6, "Przygotuj\nwymian\u0119/zwrot")
+ draw_line(ax, cx - 4, dec_y, left_x, dec_y)
+ draw_arrow(ax, left_x, dec_y, left_x, branch_y + 3)
+ ax.text(left_x + 2, dec_y + 1.5, "[tak]", fontsize=8, fontstyle="italic")
+
+ # Right [nie]
+ right_x = cx + 24
+ draw_rounded_rect(ax, right_x, branch_y, 22, 6, "Odrzu\u0107\nreklamacj\u0119")
+ draw_line(ax, cx + 4, dec_y, right_x, dec_y)
+ draw_arrow(ax, right_x, dec_y, right_x, branch_y + 3)
+ ax.text(right_x - 12, dec_y + 1.5, "[nie]", fontsize=8, fontstyle="italic")
+
+ # Merge
+ merge_y = branch_y - step
+ draw_diamond(ax, cx, merge_y, 4)
+ draw_line(ax, left_x, branch_y - 3, left_x, merge_y)
+ draw_line(ax, left_x, merge_y, cx - 4, merge_y)
+ draw_line(ax, right_x, branch_y - 3, right_x, merge_y)
+ draw_line(ax, right_x, merge_y, cx + 4, merge_y)
+
+ # Action: Powiadom
+ y = merge_y - step
+ draw_rounded_rect(ax, cx, y, 28, 6, "Powiadom klienta")
+ draw_arrow(ax, cx, merge_y - 4, cx, y + 3)
+
+ # Final node
+ ey = y - step
+ ax.add_patch(plt.Circle((cx, ey), 2.5, lw=2, facecolor="white", edgecolor="black"))
+ ax.add_patch(plt.Circle((cx, ey), 1.5, facecolor="black", edgecolor="black"))
+ draw_arrow(ax, cx, y - 3, cx, ey + 2.5)
+
+ # Legend
+ ly = 5
+ ax.add_patch(plt.Circle((12, ly), 1.2, facecolor="black", edgecolor="black"))
+ ax.text(15, ly, "= Pocz\u0105tek", fontsize=7, va="center")
+ ax.add_patch(plt.Circle((32, ly), 1.3, lw=2, facecolor="white", edgecolor="black"))
+ ax.add_patch(plt.Circle((32, ly), 0.8, facecolor="black", edgecolor="black"))
+ ax.text(35, ly, "= Koniec", fontsize=7, va="center")
+ draw_diamond(ax, 50, ly, 1.5)
+ ax.text(53, ly, "= Decyzja/Merge", fontsize=7, va="center")
+ draw_rounded_rect(ax, 78, ly, 9, 3, "Akcja", fontsize=7)
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "uml_activity_reklamacja.png"),
+ dpi=DPI,
+ facecolor="white",
+ bbox_inches="tight",
+ )
+ plt.close(fig)
+ print(" OK UML Activity saved")
+
+
+# =========================================================================
+# 3. EPC (Event-driven Process Chain)
+# =========================================================================
+def generate_epc() -> None:
+ """Generate epc."""
+ fig, ax = plt.subplots(figsize=(8.27, 11))
+ ax.set_xlim(0, 100)
+ ax.set_ylim(0, 120)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG_COLOR)
+ ax.set_title(
+ "EPC (Event-driven Process Chain) \u2014 Obs\u0142uga reklamacji",
+ fontsize=TITLE_SIZE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ cx = 50
+ y = 114
+ step = 9.5
+
+ def draw_epc_event(x, y, text) -> None:
+ """Draw epc event."""
+ w, h = 26, 5.5
+ rect = FancyBboxPatch(
+ (x - w / 2, y - h / 2),
+ w,
+ h,
+ boxstyle="round,pad=0.5",
+ lw=1.5,
+ edgecolor=LINE_COLOR,
+ facecolor="#D8D8D8",
+ )
+ ax.add_patch(rect)
+ ax.text(x, y, text, ha="center", va="center", fontsize=8)
+
+ def draw_epc_function(x, y, text) -> None:
+ """Draw epc function."""
+ w, h = 26, 5.5
+ rect = FancyBboxPatch(
+ (x - w / 2, y - h / 2),
+ w,
+ h,
+ boxstyle="round,pad=0.3",
+ lw=2,
+ edgecolor=LINE_COLOR,
+ facecolor="white",
+ )
+ ax.add_patch(rect)
+ ax.text(x, y, text, ha="center", va="center", fontsize=8, fontweight="bold")
+
+ def draw_epc_connector(x, y, text) -> None:
+ """Draw epc connector."""
+ c = plt.Circle((x, y), 2.8, lw=1.5, edgecolor=LINE_COLOR, facecolor="white")
+ ax.add_patch(c)
+ ax.text(x, y, text, ha="center", va="center", fontsize=9, fontweight="bold")
+
+ # E1
+ draw_epc_event(cx, y, "Reklamacja wp\u0142yn\u0119\u0142a")
+
+ # F1
+ y -= step
+ draw_epc_function(cx, y, "Przyjmij zg\u0142oszenie")
+ draw_arrow(ax, cx, y + step - 2.8, cx, y + 2.8)
+
+ # E2
+ y -= step
+ draw_epc_event(cx, y, "Zg\u0142oszenie przyj\u0119te")
+ draw_arrow(ax, cx, y + step - 2.8, cx, y + 2.8)
+
+ # F2
+ y -= step
+ draw_epc_function(cx, y, "Zweryfikuj zasadno\u015b\u0107")
+ draw_arrow(ax, cx, y + step - 2.8, cx, y + 2.8)
+
+ # E3
+ y -= step
+ draw_epc_event(cx, y, "Zasadno\u015b\u0107 oceniona")
+ draw_arrow(ax, cx, y + step - 2.8, cx, y + 2.8)
+
+ # XOR split
+ y -= step
+ draw_epc_connector(cx, y, "XOR")
+ draw_arrow(ax, cx, y + step - 2.8, cx, y + 2.8)
+ split_y = y
+
+ left_x = cx - 28
+ right_x = cx + 28
+
+ # Left branch
+ by = split_y - step
+ draw_epc_event(left_x, by, "Reklamacja zasadna")
+ draw_line(ax, cx - 2.8, split_y, left_x, split_y)
+ draw_arrow(ax, left_x, split_y, left_x, by + 2.8)
+
+ by2 = by - step
+ draw_epc_function(left_x, by2, "Przygotuj wymian\u0119/zwrot")
+ draw_arrow(ax, left_x, by - 2.8, left_x, by2 + 2.8)
+
+ by3 = by2 - step
+ draw_epc_event(left_x, by3, "Wymiana przygotowana")
+ draw_arrow(ax, left_x, by2 - 2.8, left_x, by3 + 2.8)
+
+ # Right branch
+ draw_epc_event(right_x, by, "Reklamacja niezasadna")
+ draw_line(ax, cx + 2.8, split_y, right_x, split_y)
+ draw_arrow(ax, right_x, split_y, right_x, by + 2.8)
+
+ draw_epc_function(right_x, by2, "Odrzu\u0107 reklamacj\u0119")
+ draw_arrow(ax, right_x, by - 2.8, right_x, by2 + 2.8)
+
+ draw_epc_event(right_x, by3, "Reklamacja odrzucona")
+ draw_arrow(ax, right_x, by2 - 2.8, right_x, by3 + 2.8)
+
+ # XOR merge
+ merge_y = by3 - step
+ draw_epc_connector(cx, merge_y, "XOR")
+ draw_line(ax, left_x, by3 - 2.8, left_x, merge_y)
+ draw_line(ax, left_x, merge_y, cx - 2.8, merge_y)
+ draw_line(ax, right_x, by3 - 2.8, right_x, merge_y)
+ draw_line(ax, right_x, merge_y, cx + 2.8, merge_y)
+
+ # F: Powiadom
+ y = merge_y - step
+ draw_epc_function(cx, y, "Powiadom klienta")
+ draw_arrow(ax, cx, merge_y - 2.8, cx, y + 2.8)
+
+ # E: Klient powiadomiony
+ y -= step
+ draw_epc_event(cx, y, "Klient powiadomiony")
+ draw_arrow(ax, cx, y + step - 2.8, cx, y + 2.8)
+
+ # Legend
+ ly = 3
+ draw_epc_event(16, ly, "Zdarzenie")
+ draw_epc_function(46, ly, "Funkcja")
+ draw_epc_connector(68, ly, "XOR")
+ ax.text(72, ly, "= \u0141\u0105cznik logiczny", fontsize=7, va="center")
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "epc_reklamacja.png"),
+ dpi=DPI,
+ facecolor="white",
+ bbox_inches="tight",
+ )
+ plt.close(fig)
+ print(" OK EPC saved")
+
+
+# =========================================================================
+# 4. Classic Flowchart
+# =========================================================================
+def generate_flowchart() -> None:
+ """Generate flowchart."""
+ fig, ax = plt.subplots(figsize=(8.27, 11))
+ ax.set_xlim(0, 100)
+ ax.set_ylim(0, 110)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ fig.patch.set_facecolor(BG_COLOR)
+ ax.set_title(
+ "Schemat blokowy (Flowchart) \u2014 Obs\u0142uga reklamacji",
+ fontsize=TITLE_SIZE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ cx = 50
+ y = 103
+ step = 11
+
+ def draw_terminal(x, y, text) -> None:
+ """Draw terminal."""
+ w, h = 20, 5.5
+ rect = FancyBboxPatch(
+ (x - w / 2, y - h / 2),
+ w,
+ h,
+ boxstyle="round,pad=1.0",
+ lw=2,
+ edgecolor=LINE_COLOR,
+ facecolor="#E0E0E0",
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x, y, text, ha="center", va="center", fontsize=FONT_SIZE, fontweight="bold"
+ )
+
+ def draw_process_box(x, y, text) -> None:
+ """Draw process box."""
+ w, h = 26, 6
+ rect = plt.Rectangle(
+ (x - w / 2, y - h / 2),
+ w,
+ h,
+ lw=1.5,
+ edgecolor=LINE_COLOR,
+ facecolor="white",
+ )
+ ax.add_patch(rect)
+ ax.text(x, y, text, ha="center", va="center", fontsize=FONT_SIZE)
+
+ def draw_io_shape(x, y, text) -> None:
+ """Draw io shape."""
+ w, h = 26, 5.5
+ skew = 3
+ verts = [
+ (x - w / 2 + skew, y + h / 2),
+ (x + w / 2 + skew, y + h / 2),
+ (x + w / 2 - skew, y - h / 2),
+ (x - w / 2 - skew, y - h / 2),
+ (x - w / 2 + skew, y + h / 2),
+ ]
+ codes = [
+ MplPath.MOVETO,
+ MplPath.LINETO,
+ MplPath.LINETO,
+ MplPath.LINETO,
+ MplPath.CLOSEPOLY,
+ ]
+ patch = mpatches.PathPatch(
+ MplPath(verts, codes), facecolor="white", edgecolor=LINE_COLOR, lw=1.5
+ )
+ ax.add_patch(patch)
+ ax.text(x, y, text, ha="center", va="center", fontsize=FONT_SIZE)
+
+ # Start
+ draw_terminal(cx, y, "START")
+
+ # I/O
+ y -= step
+ draw_io_shape(cx, y, "Reklamacja od klienta")
+ draw_arrow(ax, cx, y + step - 2.8, cx, y + 2.8)
+
+ # Process
+ y -= step
+ draw_process_box(cx, y, "Przyjmij zg\u0142oszenie")
+ draw_arrow(ax, cx, y + step - 2.8, cx, y + 3)
+
+ # Process
+ y -= step
+ draw_process_box(cx, y, "Zweryfikuj zasadno\u015b\u0107")
+ draw_arrow(ax, cx, y + step - 3, cx, y + 3)
+
+ # Decision
+ y -= step
+ draw_diamond(ax, cx, y, 4.5, "Zasadna?")
+ draw_arrow(ax, cx, y + step - 3, cx, y + 4.5)
+ dec_y = y
+
+ # Left: Tak
+ left_x = cx - 26
+ draw_process_box(left_x, dec_y, "Przygotuj wymian\u0119/zwrot")
+ draw_line(ax, cx - 4.5, dec_y, left_x + 13, dec_y)
+ ax.text(cx - 7, dec_y + 2, "Tak", fontsize=8, ha="center", fontweight="bold")
+
+ # Right: Nie
+ right_x = cx + 26
+ draw_process_box(right_x, dec_y, "Odrzu\u0107 reklamacj\u0119")
+ draw_line(ax, cx + 4.5, dec_y, right_x - 13, dec_y)
+ ax.text(cx + 7, dec_y + 2, "Nie", fontsize=8, ha="center", fontweight="bold")
+
+ # Merge
+ merge_y = dec_y - step
+ draw_line(ax, left_x, dec_y - 3, left_x, merge_y)
+ draw_line(ax, right_x, dec_y - 3, right_x, merge_y)
+ draw_line(ax, left_x, merge_y, right_x, merge_y)
+ ax.plot(cx, merge_y, "ko", markersize=4)
+
+ # Process: Powiadom
+ y = merge_y - step + 3
+ draw_process_box(cx, y, "Powiadom klienta")
+ draw_arrow(ax, cx, merge_y, cx, y + 3)
+
+ # I/O
+ y -= step
+ draw_io_shape(cx, y, "Odpowied\u017a do klienta")
+ draw_arrow(ax, cx, y + step - 3, cx, y + 2.8)
+
+ # End
+ y -= step
+ draw_terminal(cx, y, "KONIEC")
+ draw_arrow(ax, cx, y + step - 2.8, cx, y + 2.8)
+
+ # Legend
+ ly = 4
+ ax.text(5, ly, "Legenda:", fontsize=7, fontweight="bold", va="center")
+ draw_terminal(18, ly, "")
+ ax.text(18, ly, "Start/\nKoniec", fontsize=5.5, ha="center", va="center")
+ w, h = 9, 3
+ ax.add_patch(
+ plt.Rectangle(
+ (32 - w / 2, ly - h / 2),
+ w,
+ h,
+ lw=1.5,
+ edgecolor=LINE_COLOR,
+ facecolor="white",
+ )
+ )
+ ax.text(32, ly, "Proces", fontsize=6, ha="center", va="center")
+ draw_diamond(ax, 46, ly, 2)
+ ax.text(49.5, ly, "= Decyzja", fontsize=6, va="center")
+ skew = 1.5
+ w2, h2 = 9, 3
+ verts = [
+ (62 - w2 / 2 + skew, ly + h2 / 2),
+ (62 + w2 / 2 + skew, ly + h2 / 2),
+ (62 + w2 / 2 - skew, ly - h2 / 2),
+ (62 - w2 / 2 - skew, ly - h2 / 2),
+ (62 - w2 / 2 + skew, ly + h2 / 2),
+ ]
+ codes = [
+ MplPath.MOVETO,
+ MplPath.LINETO,
+ MplPath.LINETO,
+ MplPath.LINETO,
+ MplPath.CLOSEPOLY,
+ ]
+ ax.add_patch(
+ mpatches.PathPatch(
+ MplPath(verts, codes), facecolor="white", edgecolor=LINE_COLOR, lw=1.2
+ )
+ )
+ ax.text(62, ly, "We/Wy", fontsize=6, ha="center", va="center")
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "flowchart_reklamacja.png"),
+ dpi=DPI,
+ facecolor="white",
+ bbox_inches="tight",
+ )
+ plt.close(fig)
+ print(" OK Flowchart saved")
+
+
+# =========================================================================
+if __name__ == "__main__":
+ print(f"Generating diagrams to {OUTPUT_DIR}/...")
+ generate_bpmn()
+ generate_uml_activity()
+ generate_epc()
+ generate_flowchart()
+ print(f"\nAll 4 diagrams saved to {OUTPUT_DIR}/")
+ for f in sorted([p.name for p in Path(OUTPUT_DIR).iterdir()]):
+ if f.endswith(".png"):
+ size_kb = Path(str(Path(OUTPUT_DIR).stat().st_size / f)) / 1024
+ print(f" {f} ({size_kb:.0f} KB)")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_pubsub_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_pubsub_diagrams.py
new file mode 100755
index 0000000..3a0f2d8
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_pubsub_diagrams.py
@@ -0,0 +1,747 @@
+#!/usr/bin/env python3
+"""Generate Pub/Sub diagrams for PYTANIE 19.
+
+ Subscription types (4 separate images):
+ 1. Topic-based
+ 2. Content-based
+ 3. Type-based
+ 4. Hierarchical (wildcards)
+ Delivery guarantees (3 separate images):
+ 5. At-most-once
+ 6. At-least-once
+ 7. Exactly-once.
+
+All: A4-width, B&W, 300 DPI, laser-printer-friendly.
+One diagram per image — no cramming.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 9
+FS_TITLE = 13
+FIG_W = 8.27 # A4 width in inches
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.2,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=True,
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y), w, h, boxstyle="round,pad=0.05", lw=lw, edgecolor=LN, facecolor=fill
+ )
+ else:
+ rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+def draw_arrow(
+ ax,
+ x1,
+ y1,
+ x2,
+ y2,
+ lw=1.2,
+ style="->",
+ color=LN,
+ label="",
+ label_offset=0.15,
+ label_fs=8,
+) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ )
+ if label:
+ mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_offset
+ ax.text(mx, my, label, ha="center", va="bottom", fontsize=label_fs, color=color)
+
+
+def draw_dashed_arrow(
+ ax, x1, y1, x2, y2, lw=1.0, color=LN, label="", label_offset=0.15, label_fs=8
+) -> None:
+ """Draw dashed arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": color,
+ "lw": lw,
+ "linestyle": "dashed",
+ },
+ )
+ if label:
+ mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_offset
+ ax.text(mx, my, label, ha="center", va="bottom", fontsize=label_fs, color=color)
+
+
+def draw_cross(ax, x, y, size=0.15, lw=2.5, color="black") -> None:
+ """Draw cross."""
+ ax.plot([x - size, x + size], [y - size, y + size], color=color, lw=lw)
+ ax.plot([x - size, x + size], [y + size, y - size], color=color, lw=lw)
+
+
+def draw_check(ax, x, y, size=0.15, lw=2.5, color="black") -> None:
+ """Draw check."""
+ ax.plot([x - size, x - size * 0.2], [y, y - size * 0.7], color=color, lw=lw)
+ ax.plot(
+ [x - size * 0.2, x + size], [y - size * 0.7, y + size * 0.5], color=color, lw=lw
+ )
+
+
+def save(fig, name) -> None:
+ """Save."""
+ plt.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / name), dpi=DPI, bbox_inches="tight", facecolor=BG
+ )
+ plt.close(fig)
+ print(f" ✓ {name}")
+
+
+# ============================================================
+# 1. Topic-based subscription
+# ============================================================
+def draw_sub_topic() -> None:
+ """Draw sub topic."""
+ fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 4.0))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 5.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Subskrypcja topic-based — routing po nazwie tematu",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Publisher + messages
+ draw_box(
+ ax, 0.2, 3.2, 2.4, 1.1, "Publisher", fill=GRAY1, fontsize=10, fontweight="bold"
+ )
+ draw_box(ax, 0.3, 1.8, 2.2, 0.8, 'topic: "orders"', fill=GRAY4, fontsize=8)
+ draw_box(ax, 0.3, 0.7, 2.2, 0.8, 'topic: "payments"', fill=GRAY4, fontsize=8)
+
+ # Broker
+ draw_box(
+ ax,
+ 4.2,
+ 1.5,
+ 2.8,
+ 2.2,
+ "BROKER\n\ntopic routing",
+ fill=GRAY2,
+ fontsize=10,
+ fontweight="bold",
+ )
+
+ # Subscribers
+ draw_box(
+ ax,
+ 8.5,
+ 3.8,
+ 3.0,
+ 1.0,
+ 'Subscriber A\nsubskrybuje: "orders"',
+ fill=GRAY1,
+ fontsize=8.5,
+ )
+ draw_box(
+ ax,
+ 8.5,
+ 2.2,
+ 3.0,
+ 1.0,
+ 'Subscriber B\nsubskrybuje: "payments"',
+ fill=GRAY1,
+ fontsize=8.5,
+ )
+ draw_box(
+ ax,
+ 8.5,
+ 0.6,
+ 3.0,
+ 1.0,
+ 'Subscriber C\nsubskrybuje: "orders"',
+ fill=GRAY1,
+ fontsize=8.5,
+ )
+
+ # Arrows: publisher → broker
+ draw_arrow(ax, 2.6, 2.2, 4.2, 2.8, label_fs=8)
+ draw_arrow(ax, 2.6, 1.1, 4.2, 2.2, label_fs=8)
+
+ # Arrows: broker → subscribers
+ draw_arrow(ax, 7.0, 3.4, 8.5, 4.2, label='"orders"', label_fs=8)
+ draw_arrow(ax, 7.0, 2.6, 8.5, 2.7, label='"payments"', label_fs=8)
+ draw_arrow(ax, 7.0, 2.2, 8.5, 1.2, label='"orders"', label_fs=8)
+
+ # Explanation
+ ax.text(
+ 6.0,
+ 0.1,
+ "Subscriber deklaruje nazwę tematu. Broker kieruje wiadomości\n"
+ "do WSZYSTKICH subscriberów danego tematu. Najprostszy model.",
+ ha="center",
+ va="bottom",
+ fontsize=8.5,
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save(fig, "pubsub_sub_topic.png")
+
+
+# ============================================================
+# 2. Content-based subscription
+# ============================================================
+def draw_sub_content() -> None:
+ """Draw sub content."""
+ fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 4.5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 6)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Subskrypcja content-based — filtrowanie po treści wiadomości",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Publisher + message
+ draw_box(
+ ax, 0.2, 3.5, 2.4, 1.1, "Publisher", fill=GRAY1, fontsize=10, fontweight="bold"
+ )
+ draw_box(
+ ax,
+ 0.2,
+ 1.8,
+ 2.4,
+ 1.2,
+ 'price: 150\ntype: "book"\ncategory: "IT"',
+ fill=GRAY4,
+ fontsize=8.5,
+ )
+
+ # Broker
+ draw_box(
+ ax,
+ 4.0,
+ 2.0,
+ 3.0,
+ 2.5,
+ "BROKER\n\newaluuje filtry\nkażdego subscribera",
+ fill=GRAY2,
+ fontsize=9,
+ fontweight="bold",
+ )
+
+ # Subscribers with filters
+ draw_box(
+ ax, 8.5, 4.2, 3.2, 1.0, "Sub A\nfiltr: price > 100", fill=GRAY1, fontsize=9
+ )
+ draw_box(
+ ax, 8.5, 2.6, 3.2, 1.0, 'Sub B\nfiltr: type = "food"', fill=GRAY1, fontsize=9
+ )
+ draw_box(ax, 8.5, 1.0, 3.2, 1.0, "Sub C\nfiltr: price < 50", fill=GRAY1, fontsize=9)
+
+ # Arrows
+ draw_arrow(ax, 2.6, 2.4, 4.0, 3.0)
+ draw_arrow(ax, 7.0, 4.0, 8.5, 4.6, label="150 > 100 ✓ dostarczono", label_fs=8)
+ draw_dashed_arrow(
+ ax, 7.0, 3.2, 8.5, 3.1, label='"book" ≠ "food" ✗ odrzucono', label_fs=8
+ )
+ draw_dashed_arrow(
+ ax, 7.0, 2.5, 8.5, 1.6, label="150 < 50 ✗ odrzucono", label_fs=8
+ )
+
+ ax.text(
+ 6.0,
+ 0.2,
+ "Broker analizuje TREŚĆ wiadomości i ewaluuje predykaty.\n"
+ "Bardziej elastyczny niż topic-based, ale wolniejszy (koszt ewaluacji).",
+ ha="center",
+ va="bottom",
+ fontsize=8.5,
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save(fig, "pubsub_sub_content.png")
+
+
+# ============================================================
+# 3. Type-based subscription
+# ============================================================
+def draw_sub_type() -> None:
+ """Draw sub type."""
+ fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.0))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 6.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Subskrypcja type-based — routing po typie (klasie) obiektu",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Publisher
+ draw_box(
+ ax, 0.2, 4.2, 2.4, 1.1, "Publisher", fill=GRAY1, fontsize=10, fontweight="bold"
+ )
+
+ # Messages
+ draw_box(ax, 0.1, 2.8, 2.6, 0.9, "new OrderEvent()", fill=GRAY4, fontsize=9)
+ draw_box(ax, 0.1, 1.5, 2.6, 0.9, "new PaymentEvent()", fill=GRAY4, fontsize=9)
+
+ # Broker
+ draw_box(
+ ax,
+ 4.0,
+ 2.3,
+ 3.0,
+ 2.4,
+ "BROKER\n\nrouting po\ntypie klasy",
+ fill=GRAY2,
+ fontsize=10,
+ fontweight="bold",
+ )
+
+ # Subscribers
+ draw_box(ax, 8.5, 4.8, 3.2, 1.0, "Sub A\n→ OrderEvent", fill=GRAY1, fontsize=9)
+ draw_box(ax, 8.5, 3.2, 3.2, 1.0, "Sub B\n→ PaymentEvent", fill=GRAY1, fontsize=9)
+ draw_box(ax, 8.5, 1.6, 3.2, 1.0, "Sub C\n→ Event (base)", fill=GRAY1, fontsize=9)
+
+ # Arrows
+ draw_arrow(ax, 2.7, 3.2, 4.0, 3.8)
+ draw_arrow(ax, 2.7, 2.0, 4.0, 3.0)
+ draw_arrow(ax, 7.0, 4.3, 8.5, 5.2, label="OrderEvent", label_fs=8)
+ draw_arrow(ax, 7.0, 3.5, 8.5, 3.7, label="PaymentEvent", label_fs=8)
+ draw_arrow(ax, 7.0, 3.0, 8.5, 2.2, label="oba (dziedziczenie!)", label_fs=8)
+
+ # Class hierarchy inset
+ hx, hy = 0.5, 0.0
+ draw_box(
+ ax,
+ hx + 2.0,
+ hy + 0.2,
+ 1.8,
+ 0.6,
+ "Event",
+ fill=GRAY3,
+ fontsize=8,
+ fontweight="bold",
+ )
+ draw_box(ax, hx + 0.0, hy + 0.2, 1.8, 0.6, "OrderEvent", fill=GRAY4, fontsize=7.5)
+ draw_box(ax, hx + 4.0, hy + 0.2, 2.0, 0.6, "PaymentEvent", fill=GRAY4, fontsize=7.5)
+ draw_arrow(
+ ax,
+ hx + 2.9,
+ hy + 0.2,
+ hx + 0.9,
+ hy + 0.2,
+ lw=1.0,
+ style="->",
+ label="extends",
+ label_offset=-0.3,
+ label_fs=7,
+ )
+ draw_arrow(
+ ax,
+ hx + 2.9,
+ hy + 0.2,
+ hx + 5.0,
+ hy + 0.2,
+ lw=1.0,
+ style="->",
+ label="extends",
+ label_offset=-0.3,
+ label_fs=7,
+ )
+
+ ax.text(
+ 9.5,
+ 0.5,
+ "Sub C subskrybuje bazowy Event\n→ otrzymuje WSZYSTKIE podtypy",
+ ha="center",
+ va="center",
+ fontsize=8.5,
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save(fig, "pubsub_sub_type.png")
+
+
+# ============================================================
+# 4. Hierarchical / Wildcards subscription
+# ============================================================
+def draw_sub_hierarchical() -> None:
+ """Draw sub hierarchical."""
+ fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Subskrypcja hierarchiczna (wildcards) — wzorce tematów",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Topic tree
+ draw_box(
+ ax, 4.5, 5.8, 2.4, 0.8, "sensors/", fill=GRAY2, fontsize=10, fontweight="bold"
+ )
+
+ draw_box(ax, 1.5, 4.2, 2.4, 0.8, "temperature/", fill=GRAY3, fontsize=9)
+ draw_box(ax, 7.5, 4.2, 2.4, 0.8, "humidity/", fill=GRAY3, fontsize=9)
+
+ draw_box(ax, 0.2, 2.8, 1.8, 0.7, "room1", fill=GRAY4, fontsize=8.5)
+ draw_box(ax, 2.4, 2.8, 1.8, 0.7, "room2", fill=GRAY4, fontsize=8.5)
+ draw_box(ax, 6.8, 2.8, 1.8, 0.7, "room1", fill=GRAY4, fontsize=8.5)
+ draw_box(ax, 9.0, 2.8, 1.8, 0.7, "room2", fill=GRAY4, fontsize=8.5)
+
+ # Tree edges
+ draw_arrow(ax, 5.7, 5.8, 2.7, 5.0, lw=1.0)
+ draw_arrow(ax, 5.7, 5.8, 8.7, 5.0, lw=1.0)
+ draw_arrow(ax, 2.2, 4.2, 1.1, 3.5, lw=1.0)
+ draw_arrow(ax, 3.2, 4.2, 3.3, 3.5, lw=1.0)
+ draw_arrow(ax, 8.2, 4.2, 7.7, 3.5, lw=1.0)
+ draw_arrow(ax, 9.2, 4.2, 9.9, 3.5, lw=1.0)
+
+ # Full paths
+ ax.text(
+ 1.1,
+ 2.4,
+ "sensors/temperature/room1",
+ fontsize=7,
+ ha="center",
+ fontfamily="monospace",
+ style="italic",
+ )
+ ax.text(
+ 3.3,
+ 2.4,
+ "sensors/temperature/room2",
+ fontsize=7,
+ ha="center",
+ fontfamily="monospace",
+ style="italic",
+ )
+
+ # Wildcard examples
+ ax.text(
+ 0.3, 1.5, "Wzorce subskrypcji (MQTT-style):", fontsize=10, fontweight="bold"
+ )
+
+ patterns = [
+ ('"sensors/temperature/room1"', "→ TYLKO room1", "(dokładne dopasowanie)"),
+ ('"sensors/temperature/*"', "→ room1, room2", "( * = jeden poziom)"),
+ ('"sensors/#"', "→ WSZYSTKO", "( # = dowolna głębokość)"),
+ ]
+ for i, (pat, result, note) in enumerate(patterns):
+ yy = 0.9 - i * 0.55
+ ax.text(0.5, yy, pat, fontsize=9, fontweight="bold", fontfamily="monospace")
+ ax.text(7.0, yy, result, fontsize=9, fontweight="bold")
+ ax.text(9.5, yy, note, fontsize=8, style="italic")
+
+ save(fig, "pubsub_sub_hierarchical.png")
+
+
+# ============================================================
+# 5. At-most-once (QoS 0)
+# ============================================================
+def draw_qos_at_most_once() -> None:
+ """Draw qos at most once."""
+ fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 4.5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 6)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ 'QoS: At-most-once — „wyślij i zapomnij" (0 lub 1 dostarczenie)',
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Actors
+ px, bx, sx = 1.0, 4.8, 8.5
+ pw, bw, sw = 2.0, 2.2, 2.0
+ bh = 0.8
+ draw_box(
+ ax, px, 5.0, pw, bh, "Publisher", fill=GRAY1, fontsize=10, fontweight="bold"
+ )
+ draw_box(ax, bx, 5.0, bw, bh, "Broker", fill=GRAY2, fontsize=10, fontweight="bold")
+ draw_box(
+ ax, sx, 5.0, sw, bh, "Subscriber", fill=GRAY1, fontsize=10, fontweight="bold"
+ )
+
+ # Timelines
+ for xc in [px + pw / 2, bx + bw / 2, sx + sw / 2]:
+ ax.plot([xc, xc], [5.0, 1.2], color=GRAY3, lw=1, linestyle=":")
+
+ # Scenario A: success
+ y = 4.3
+ ax.text(0.2, y + 0.15, "Scenariusz A:", fontsize=8.5, fontweight="bold")
+ draw_arrow(ax, px + pw / 2, y, bx + bw / 2, y, label="MSG", label_fs=9)
+ draw_arrow(ax, bx + bw / 2, y - 0.6, sx + sw / 2, y - 0.6, label="MSG", label_fs=9)
+ draw_check(ax, sx + sw / 2 + 0.4, y - 0.6, size=0.18)
+ ax.text(sx + sw / 2 + 0.7, y - 0.6, "OK", fontsize=9, fontweight="bold")
+
+ # Scenario B: lost
+ y = 2.6
+ ax.text(0.2, y + 0.15, "Scenariusz B:", fontsize=8.5, fontweight="bold")
+ draw_arrow(ax, px + pw / 2, y, bx + bw / 2, y, label="MSG", label_fs=9)
+ draw_dashed_arrow(ax, bx + bw / 2, y - 0.6, 7.5, y - 0.6)
+ draw_cross(ax, 7.8, y - 0.6, size=0.2)
+ ax.text(8.2, y - 0.55, "UTRACONA", fontsize=9, fontweight="bold")
+ ax.text(8.2, y - 1.0, "(brak retransmisji)", fontsize=8, style="italic")
+
+ # Summary
+ ax.text(
+ 6.0,
+ 0.5,
+ "Brak ACK, brak retransmisji. Najszybszy. Use case: logi, metryki, telemetria.",
+ ha="center",
+ va="center",
+ fontsize=9,
+ bbox={"boxstyle": "round,pad=0.4", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save(fig, "pubsub_qos_at_most_once.png")
+
+
+# ============================================================
+# 6. At-least-once (QoS 1)
+# ============================================================
+def draw_qos_at_least_once() -> None:
+ """Draw qos at least once."""
+ fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.0))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 6.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ 'QoS: At-least-once — „powtarzaj aż potwierdzą" (≥1 dostarczenie)',
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ bx, bw = 3.5, 2.2
+ sx, sw = 8.0, 2.2
+ bh = 0.8
+ draw_box(ax, bx, 5.5, bw, bh, "Broker", fill=GRAY2, fontsize=10, fontweight="bold")
+ draw_box(
+ ax, sx, 5.5, sw, bh, "Subscriber", fill=GRAY1, fontsize=10, fontweight="bold"
+ )
+
+ # Timelines
+ for xc in [bx + bw / 2, sx + sw / 2]:
+ ax.plot([xc, xc], [5.5, 0.8], color=GRAY3, lw=1, linestyle=":")
+
+ # Step 1: send MSG
+ y1 = 4.8
+ draw_arrow(ax, bx + bw / 2, y1, sx + sw / 2, y1, label="MSG #1", label_fs=9)
+ draw_check(ax, sx + sw + 0.2, y1, size=0.15)
+ ax.text(sx + sw + 0.5, y1, "odebrano", fontsize=8)
+
+ # Step 2: ACK lost
+ y2 = 3.9
+ draw_dashed_arrow(ax, sx + sw / 2, y2, bx + bw + 1.2, y2)
+ ax.text((bx + bw / 2 + sx + sw / 2) / 2, y2 + 0.18, "ACK", fontsize=9)
+ draw_cross(ax, bx + bw + 0.8, y2, size=0.18)
+ ax.text(bx + 0.3, y2 - 0.35, "ACK utracony!", fontsize=8.5, style="italic")
+
+ # Step 3: timeout → retry
+ y3 = 2.9
+ ax.text(
+ bx + bw / 2, y3 + 0.45, "timeout...", fontsize=8.5, style="italic", ha="center"
+ )
+ draw_arrow(ax, bx + bw / 2, y3, sx + sw / 2, y3, label="MSG #1 (retry)", label_fs=9)
+ draw_check(ax, sx + sw + 0.2, y3, size=0.15)
+ ax.text(sx + sw + 0.5, y3, "odebrano\n(ponownie!)", fontsize=8)
+
+ # Step 4: ACK ok
+ y4 = 2.0
+ draw_arrow(ax, sx + sw / 2, y4, bx + bw / 2, y4, label="ACK", label_fs=9)
+ draw_check(ax, bx + bw / 2 - 0.5, y4, size=0.18)
+
+ # Duplicate bracket
+ ax.annotate(
+ "",
+ xy=(sx + sw + 1.3, y1),
+ xytext=(sx + sw + 1.3, y3),
+ arrowprops={"arrowstyle": "<->", "color": "black", "lw": 1.2},
+ )
+ ax.text(
+ sx + sw + 1.6,
+ (y1 + y3) / 2,
+ "DUPLIKAT!\nSubscriber\notrzymał 2x",
+ fontsize=9,
+ ha="left",
+ va="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.25", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # Summary
+ ax.text(
+ 6.0,
+ 0.5,
+ "Broker czeka na ACK, retransmituje po timeout. Mogą być duplikaty!\n"
+ "Use case: zamówienia, płatności (subscriber musi być idempotentny).",
+ ha="center",
+ va="center",
+ fontsize=9,
+ bbox={"boxstyle": "round,pad=0.4", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save(fig, "pubsub_qos_at_least_once.png")
+
+
+# ============================================================
+# 7. Exactly-once (QoS 2)
+# ============================================================
+def draw_qos_exactly_once() -> None:
+ """Draw qos exactly once."""
+ fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "QoS: Exactly-once — 4-krokowy handshake (dokładnie 1 dostarczenie)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ bx, bw = 2.5, 2.2
+ sx, sw = 7.5, 2.2
+ bh = 0.8
+ draw_box(ax, bx, 6.0, bw, bh, "Broker", fill=GRAY2, fontsize=10, fontweight="bold")
+ draw_box(
+ ax, sx, 6.0, sw, bh, "Subscriber", fill=GRAY1, fontsize=10, fontweight="bold"
+ )
+
+ # Timelines
+ for xc in [bx + bw / 2, sx + sw / 2]:
+ ax.plot([xc, xc], [6.0, 1.0], color=GRAY3, lw=1, linestyle=":")
+
+ # 4-step handshake
+ steps = [
+ (5.2, "right", "PUBLISH (msg_id=42)", "Broker wysyła wiadomość"),
+ (
+ 4.2,
+ "left",
+ "PUBREC (otrzymałem id=42)",
+ "Sub potwierdza odbiór, zapisuje id",
+ ),
+ (3.2, "right", "PUBREL (możesz przetworzyć)", "Broker zwalnia wiadomość"),
+ (2.2, "left", "PUBCOMP (zakończone)", "Sub potwierdza przetworzenie"),
+ ]
+
+ for i, (y, direction, label, desc) in enumerate(steps):
+ # Step number
+ ax.text(
+ bx + bw / 2 - 0.7,
+ y,
+ f"{i + 1}",
+ fontsize=9,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ bbox={"boxstyle": "circle,pad=0.18", "facecolor": GRAY3, "edgecolor": LN},
+ )
+
+ if direction == "right":
+ draw_arrow(ax, bx + bw / 2, y, sx + sw / 2, y, label=label, label_fs=9)
+ else:
+ draw_arrow(ax, sx + sw / 2, y, bx + bw / 2, y, label=label, label_fs=9)
+
+ # Side description
+ ax.text(
+ sx + sw + 0.3, y, desc, fontsize=8, ha="left", va="center", style="italic"
+ )
+
+ # Summary
+ ax.text(
+ 6.0,
+ 0.6,
+ "Deduplikacja po msg_id. Sub nie przetwarza przed PUBREL.\n"
+ "Najkosztowniejszy (4 pakiety). Use case: transakcje finansowe, krytyczne zdarzenia.",
+ ha="center",
+ va="center",
+ fontsize=9,
+ bbox={"boxstyle": "round,pad=0.4", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save(fig, "pubsub_qos_exactly_once.png")
+
+
+# ============================================================
+# Main
+# ============================================================
+if __name__ == "__main__":
+ print("Generating Pub/Sub diagrams (7 separate images)...")
+ draw_sub_topic()
+ draw_sub_content()
+ draw_sub_type()
+ draw_sub_hierarchical()
+ draw_qos_at_most_once()
+ draw_qos_at_least_once()
+ draw_qos_exactly_once()
+ print("Done!")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_q20_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_q20_diagrams.py
new file mode 100755
index 0000000..78eaf0b
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_q20_diagrams.py
@@ -0,0 +1,2114 @@
+#!/usr/bin/env python3
+"""Generate ALL diagrams for PYTANIE 20: Analityka danych strumieniowych.
+
+Monochrome, A4-printable PNGs (300 DPI).
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+import numpy as np
+
+rng = np.random.default_rng(42)
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 11
+FS_SMALL = 6.5
+FS_LABEL = 9
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.2,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=True,
+ edgecolor=LN,
+ linestyle="-",
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y),
+ w,
+ h,
+ boxstyle="round,pad=0.05",
+ lw=lw,
+ edgecolor=edgecolor,
+ facecolor=fill,
+ linestyle=linestyle,
+ )
+ else:
+ rect = mpatches.Rectangle(
+ (x, y),
+ w,
+ h,
+ lw=lw,
+ edgecolor=edgecolor,
+ facecolor=fill,
+ linestyle=linestyle,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ )
+
+
+def save_fig(fig, name) -> None:
+ """Save fig."""
+ path = str(Path(OUTPUT_DIR) / name)
+ fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG, pad_inches=0.15)
+ plt.close(fig)
+ print(f" Saved: {path}")
+
+
+def draw_table(
+ ax,
+ headers,
+ rows,
+ x0,
+ y0,
+ col_widths,
+ row_h=0.4,
+ header_fill=GRAY2,
+ row_fills=None,
+ fontsize=FS,
+ header_fontsize=None,
+) -> None:
+ """Draw table."""
+ if header_fontsize is None:
+ header_fontsize = fontsize
+ len(headers)
+ # Header
+ cx = x0
+ for j, hdr in enumerate(headers):
+ draw_box(
+ ax,
+ cx,
+ y0,
+ col_widths[j],
+ row_h,
+ hdr,
+ fill=header_fill,
+ fontsize=header_fontsize,
+ fontweight="bold",
+ rounded=False,
+ )
+ cx += col_widths[j]
+ # Rows
+ for i, row in enumerate(rows):
+ cy = y0 - (i + 1) * row_h
+ cx = x0
+ fill = GRAY4 if (i % 2 == 0) else "white"
+ if row_fills and i < len(row_fills):
+ fill = row_fills[i]
+ for j, cell in enumerate(row):
+ fw = "bold" if j == 0 else "normal"
+ draw_box(
+ ax,
+ cx,
+ cy,
+ col_widths[j],
+ row_h,
+ cell,
+ fill=fill,
+ fontsize=fontsize,
+ fontweight=fw,
+ rounded=False,
+ )
+ cx += col_widths[j]
+
+
+# ============================================================
+# 1. Batch vs Streaming concept
+# ============================================================
+def gen_batch_vs_streaming() -> None:
+ """Gen batch vs streaming."""
+ fig, axes = plt.subplots(2, 1, figsize=(9, 5))
+ fig.suptitle(
+ "Batch vs Streaming — dwa modele przetwarzania",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ )
+
+ # Batch
+ ax = axes[0]
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 3)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("BATCH (wsadowe)", fontsize=FS_LABEL, fontweight="bold")
+
+ # Data collected
+ draw_box(
+ ax,
+ 0.5,
+ 0.8,
+ 3.0,
+ 1.4,
+ "Zbierz WSZYSTKIE\ndane\n(godziny / dni)",
+ fill=GRAY1,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 3.5, 1.5, 4.5, 1.5, lw=2)
+ draw_box(
+ ax,
+ 4.5,
+ 0.8,
+ 2.5,
+ 1.4,
+ "Analiza\n(batch job)",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 7.0, 1.5, 8.0, 1.5, lw=2)
+ draw_box(
+ ax,
+ 8.0,
+ 0.8,
+ 2.5,
+ 1.4,
+ "Wynik\n(jednorazowy)",
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.text(11.0, 1.5, "min-h", fontsize=FS, va="center", fontweight="bold")
+
+ # Streaming
+ ax = axes[1]
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 3)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("STREAMING (strumieniowe)", fontsize=FS_LABEL, fontweight="bold")
+
+ # Events flowing
+ events_x = [0.5, 1.5, 2.5, 3.5]
+ for i, ex in enumerate(events_x):
+ draw_box(
+ ax,
+ ex,
+ 1.0,
+ 0.8,
+ 0.8,
+ f"e{i + 1}",
+ fill=GRAY4,
+ fontsize=FS,
+ fontweight="bold",
+ rounded=False,
+ )
+ if i < len(events_x) - 1:
+ draw_arrow(ax, ex + 0.8, 1.4, ex + 1.0, 1.4, lw=1)
+
+ ax.text(4.8, 1.4, "...", fontsize=FS_LABEL, va="center")
+ draw_arrow(ax, 5.2, 1.4, 5.8, 1.4, lw=2)
+
+ draw_box(
+ ax,
+ 5.8,
+ 0.8,
+ 2.8,
+ 1.4,
+ "Analiza\nCIĄGŁA\n(event-by-event)",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 8.6, 1.5, 9.3, 1.5, lw=2)
+ draw_box(
+ ax,
+ 9.3,
+ 0.8,
+ 2.0,
+ 1.4,
+ "Wyniki\nciągłe",
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.text(11.5, 0.5, "ms-s", fontsize=FS, va="center", fontweight="bold")
+
+ # Arrow marking infinity
+ ax.annotate(
+ "",
+ xy=(0.2, 1.4),
+ xytext=(-0.3, 1.4),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+ ax.text(0.0, 2.3, "∞ zdarzeń", fontsize=FS_SMALL, ha="center", style="italic")
+
+ fig.tight_layout(rect=[0, 0, 1, 0.92])
+ save_fig(fig, "q20_batch_vs_streaming.png")
+
+
+# ============================================================
+# 2. All 4 window types (TSSG)
+# ============================================================
+def gen_window_types() -> None:
+ """Gen window types."""
+ fig, axes = plt.subplots(4, 1, figsize=(9, 10))
+ fig.suptitle("4 typy okien — TSSG", fontsize=FS_TITLE, fontweight="bold")
+
+ # Events on a timeline (shared concept)
+ events = list(range(1, 13))
+
+ # --- Tumbling ---
+ ax = axes[0]
+ ax.set_xlim(0, 14)
+ ax.set_ylim(0, 4)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Tumbling Window (okno przerzutne) — rozłączne, stały rozmiar",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ )
+
+ # Time axis
+ ax.annotate(
+ "",
+ xy=(13.5, 1.0),
+ xytext=(0.3, 1.0),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+ ax.text(13.5, 0.6, "czas", fontsize=FS_SMALL, ha="center")
+
+ # Events
+ for i, e in enumerate(events):
+ x = 1.0 + i * 1.0
+ ax.plot(x, 1.0, "ko", markersize=5)
+ ax.text(x, 0.5, f"e{e}", fontsize=FS_SMALL, ha="center")
+
+ # Windows
+ colors_w = [GRAY1, GRAY3, GRAY1, GRAY3]
+ for w in range(4):
+ x_start = 1.0 + w * 3.0 - 0.3
+ rect = mpatches.FancyBboxPatch(
+ (x_start, 1.5),
+ 3.0,
+ 1.2,
+ boxstyle="round,pad=0.1",
+ facecolor=colors_w[w],
+ edgecolor=LN,
+ lw=1.5,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x_start + 1.5,
+ 2.1,
+ f"Okno {w + 1}",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ )
+ # Braces down to events
+ for j in range(3):
+ ex = 1.0 + w * 3.0 + j * 1.0
+ ax.plot([ex, ex], [1.0, 1.5], color=LN, lw=0.8, linestyle="--")
+
+ ax.text(
+ 7.0,
+ 3.2,
+ "Każde zdarzenie → DOKŁADNIE 1 okno. Zero nakładania.",
+ fontsize=FS,
+ ha="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ # --- Sliding ---
+ ax = axes[1]
+ ax.set_xlim(0, 14)
+ ax.set_ylim(0, 5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Sliding Window (okno przesuwne) — nakładające, stały rozmiar + krok",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ )
+
+ ax.annotate(
+ "",
+ xy=(13.5, 1.0),
+ xytext=(0.3, 1.0),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+ ax.text(13.5, 0.6, "czas", fontsize=FS_SMALL, ha="center")
+
+ for i, e in enumerate(events[:8]):
+ x = 1.0 + i * 1.0
+ ax.plot(x, 1.0, "ko", markersize=5)
+ ax.text(x, 0.5, f"e{e}", fontsize=FS_SMALL, ha="center")
+
+ # Sliding windows: size=4, slide=2
+ slide_colors = [GRAY1, GRAY2, GRAY3]
+ for w in range(3):
+ x_start = 0.7 + w * 2.0
+ y_base = 1.5 + w * 0.9
+ rect = mpatches.FancyBboxPatch(
+ (x_start, y_base),
+ 4.0,
+ 0.7,
+ boxstyle="round,pad=0.08",
+ facecolor=slide_colors[w],
+ edgecolor=LN,
+ lw=1.5,
+ alpha=0.7,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x_start + 2.0,
+ y_base + 0.35,
+ f"Okno {w + 1} (size=4)",
+ fontsize=FS_SMALL,
+ ha="center",
+ fontweight="bold",
+ )
+
+ ax.text(
+ 10.5,
+ 3.5,
+ "krok=2\nNakładanie!\ne3,e4 → w oknie 1 i 2",
+ fontsize=FS,
+ ha="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ # --- Session ---
+ ax = axes[2]
+ ax.set_xlim(0, 14)
+ ax.set_ylim(0, 4)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Session Window (okno sesji) — dynamiczny rozmiar, gap = przerwa",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ )
+
+ ax.annotate(
+ "",
+ xy=(13.5, 1.0),
+ xytext=(0.3, 1.0),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+ ax.text(13.5, 0.6, "czas", fontsize=FS_SMALL, ha="center")
+
+ # Cluster 1: events close together
+ cluster1 = [1.0, 1.8, 2.3, 3.0]
+ for x in cluster1:
+ ax.plot(x, 1.0, "ko", markersize=5)
+
+ # Gap
+ ax.annotate(
+ "",
+ xy=(7.0, 0.7),
+ xytext=(4.0, 0.7),
+ arrowprops={"arrowstyle": "<->", "lw": 1, "color": LN},
+ )
+ ax.text(
+ 5.5,
+ 0.3,
+ "GAP > timeout",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ style="italic",
+ )
+
+ # Cluster 2
+ cluster2 = [8.0, 8.8, 9.5]
+ for x in cluster2:
+ ax.plot(x, 1.0, "ko", markersize=5)
+
+ # Session boxes
+ rect1 = mpatches.FancyBboxPatch(
+ (0.7, 1.4),
+ 2.6,
+ 1.0,
+ boxstyle="round,pad=0.1",
+ facecolor=GRAY1,
+ edgecolor=LN,
+ lw=1.5,
+ )
+ ax.add_patch(rect1)
+ ax.text(
+ 2.0, 1.9, "Sesja 1\n(4 zdarzenia)", fontsize=FS, ha="center", fontweight="bold"
+ )
+
+ rect2 = mpatches.FancyBboxPatch(
+ (7.7, 1.4),
+ 2.1,
+ 1.0,
+ boxstyle="round,pad=0.1",
+ facecolor=GRAY3,
+ edgecolor=LN,
+ lw=1.5,
+ )
+ ax.add_patch(rect2)
+ ax.text(
+ 8.75, 1.9, "Sesja 2\n(3 zdarzenia)", fontsize=FS, ha="center", fontweight="bold"
+ )
+
+ ax.text(
+ 5.5,
+ 3.0,
+ "Nowa sesja po przerwie > gap",
+ fontsize=FS,
+ ha="center",
+ style="italic",
+ )
+
+ # --- Global ---
+ ax = axes[3]
+ ax.set_xlim(0, 14)
+ ax.set_ylim(0, 4)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Global Window — jedno okno na cały strumień + trigger",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ )
+
+ ax.annotate(
+ "",
+ xy=(13.5, 1.0),
+ xytext=(0.3, 1.0),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+ ax.text(13.5, 0.6, "czas", fontsize=FS_SMALL, ha="center")
+
+ for i in range(12):
+ x = 1.0 + i * 1.0
+ ax.plot(x, 1.0, "ko", markersize=5)
+
+ # One big window
+ rect = mpatches.FancyBboxPatch(
+ (0.5, 1.4),
+ 12.5,
+ 1.0,
+ boxstyle="round,pad=0.1",
+ facecolor=GRAY1,
+ edgecolor=LN,
+ lw=2,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 6.75,
+ 1.9,
+ "GLOBAL WINDOW (cały strumień)",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ )
+
+ # Trigger markers
+ for tx in [4.0, 8.0, 12.0]:
+ ax.plot([tx, tx], [1.4, 2.4], color=LN, lw=2, linestyle="--")
+ ax.text(
+ tx,
+ 2.7,
+ "EMIT",
+ fontsize=FS_SMALL,
+ ha="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.1", "facecolor": GRAY3, "edgecolor": LN},
+ )
+
+ ax.text(
+ 6.75,
+ 3.3,
+ "Trigger decyduje kiedy emitować (np. co N zdarzeń)",
+ fontsize=FS,
+ ha="center",
+ style="italic",
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.94])
+ save_fig(fig, "q20_window_types.png")
+
+
+# ============================================================
+# 3. Event Time vs Processing Time scatter + watermark
+# ============================================================
+def gen_event_vs_processing_time() -> None:
+ """Gen event vs processing time."""
+ fig, axes = plt.subplots(1, 2, figsize=(11, 5))
+ fig.suptitle(
+ "Event Time vs Processing Time + Watermark",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ )
+
+ # --- Panel 1: Ideal vs Real ---
+ ax = axes[0]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.set_aspect("equal")
+ ax.set_xlabel("Event Time", fontsize=FS_LABEL)
+ ax.set_ylabel("Processing Time", fontsize=FS_LABEL)
+ ax.set_title("Idealny vs Realny świat", fontsize=FS_LABEL, fontweight="bold")
+ ax.set_xticks([])
+ ax.set_yticks([])
+
+ # Ideal line
+ ax.plot([0, 9], [0, 9], "k--", lw=1.5, label="ideał (brak opóźnień)")
+
+ # Real scattered points (processing >= event, some out of order)
+ event_times = np.sort(rng.uniform(1, 8, 15))
+ proc_times = event_times + rng.exponential(0.5, 15)
+ # Make some out of order
+ idx = [3, 7, 11]
+ for i in idx:
+ proc_times[i] += 1.5
+
+ ax.scatter(
+ event_times, proc_times, c="black", s=30, zorder=5, label="zdarzenia (realne)"
+ )
+
+ # Highlight out-of-order
+ for i in idx:
+ ax.annotate(
+ "out-of-order",
+ xy=(event_times[i], proc_times[i]),
+ xytext=(event_times[i] + 0.8, proc_times[i] + 0.5),
+ fontsize=FS_SMALL,
+ ha="left",
+ arrowprops={"arrowstyle": "->", "lw": 0.8, "color": "#555"},
+ )
+
+ ax.legend(fontsize=FS_SMALL, loc="upper left")
+ ax.text(
+ 7,
+ 2,
+ "Opóźnienie\nsieciowe ↑",
+ fontsize=FS,
+ ha="center",
+ style="italic",
+ color="#555",
+ )
+
+ # --- Panel 2: Watermark concept ---
+ ax = axes[1]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.set_aspect("equal")
+ ax.set_xlabel("Event Time", fontsize=FS_LABEL)
+ ax.set_ylabel("Processing Time", fontsize=FS_LABEL)
+ ax.set_title("Watermark — granica postępu", fontsize=FS_LABEL, fontweight="bold")
+ ax.set_xticks([])
+ ax.set_yticks([])
+
+ # Events
+ ax.scatter(event_times, proc_times, c="black", s=30, zorder=5)
+
+ # Watermark line (below most points, tracks progress)
+ wm_x = np.linspace(0, 9, 50)
+ wm_y = wm_x + 0.3 # watermark slightly above ideal
+ ax.plot(wm_x, wm_y, "k-", lw=2.5, label="Watermark")
+ ax.fill_between(wm_x, 0, wm_y, alpha=0.15, color="gray")
+
+ ax.text(
+ 2.0,
+ 1.0,
+ 'PONIŻEJ watermark:\n„na pewno dotarło"',
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ # Late event
+ late_x, late_y = event_times[7], proc_times[7]
+ ax.scatter(
+ [late_x], [late_y], c="white", s=80, zorder=6, edgecolors="black", linewidths=2
+ )
+ ax.annotate(
+ "LATE DATA!\n(po watermarku)",
+ xy=(late_x, late_y),
+ xytext=(late_x + 1.2, late_y + 0.8),
+ fontsize=FS_SMALL,
+ ha="left",
+ fontweight="bold",
+ arrowprops={"arrowstyle": "->", "lw": 1, "color": LN},
+ )
+
+ ax.legend(fontsize=FS_SMALL, loc="upper left")
+
+ fig.tight_layout(rect=[0, 0, 1, 0.92])
+ save_fig(fig, "q20_event_vs_processing_time.png")
+
+
+# ============================================================
+# 4. Tumbling window example — fraud detection
+# ============================================================
+def gen_tumbling_fraud() -> None:
+ """Gen tumbling fraud."""
+ fig, ax = plt.subplots(figsize=(9, 4))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 5.5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Tumbling Window — fraud detection (okno = 1 min)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Time axis
+ ax.annotate(
+ "",
+ xy=(11.5, 1.0),
+ xytext=(0.5, 1.0),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+ ax.text(6.0, 0.4, "czas", fontsize=FS, ha="center")
+
+ # Window 1: normal
+ draw_box(ax, 1.0, 1.5, 4.5, 3.0, "", fill=GRAY4, rounded=True, lw=2)
+ ax.text(
+ 3.25, 4.2, "[14:00 — 14:01]", fontsize=FS_LABEL, ha="center", fontweight="bold"
+ )
+ # Transactions
+ txns1 = ["Sklep A: 50 zł", "Sklep B: 30 zł", "Stacja: 80 zł"]
+ for i, t in enumerate(txns1):
+ draw_box(
+ ax,
+ 1.3,
+ 3.3 - i * 0.55,
+ 4.0,
+ 0.45,
+ t,
+ fill=GRAY1,
+ fontsize=FS_SMALL,
+ rounded=False,
+ )
+ ax.text(
+ 3.25,
+ 1.7,
+ "count = 3 → OK",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ color="#2E7D32",
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "#E8F5E9",
+ "edgecolor": "#2E7D32",
+ },
+ )
+
+ # Window 2: fraud!
+ draw_box(ax, 6.0, 1.5, 4.5, 3.0, "", fill=GRAY1, rounded=True, lw=2)
+ ax.text(
+ 8.25, 4.2, "[14:01 — 14:02]", fontsize=FS_LABEL, ha="center", fontweight="bold"
+ )
+ txns2 = ["ATM Warszawa: 500 zł", "ATM Kraków: 500 zł", "... +45 transakcji"]
+ for i, t in enumerate(txns2):
+ draw_box(
+ ax,
+ 6.3,
+ 3.3 - i * 0.55,
+ 4.0,
+ 0.45,
+ t,
+ fill=GRAY3,
+ fontsize=FS_SMALL,
+ rounded=False,
+ )
+ ax.text(
+ 8.25,
+ 1.7,
+ "count = 47 → ALERT!",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ color="#C62828",
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "#F8D7DA",
+ "edgecolor": "#C62828",
+ },
+ )
+
+ save_fig(fig, "q20_tumbling_fraud.png")
+
+
+# ============================================================
+# 5. Sliding window — SLA monitoring
+# ============================================================
+def gen_sliding_sla() -> None:
+ """Gen sliding sla."""
+ fig, ax = plt.subplots(figsize=(9, 4.5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 6)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Sliding Window — monitoring SLA (okno=5min, krok=1min)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Time axis
+ ax.annotate(
+ "",
+ xy=(11.5, 0.5),
+ xytext=(0.5, 0.5),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+ times = ["14:05", "14:06", "14:07", "14:08", "14:09"]
+ latencies = [120, 180, 340, 290, 150]
+ sla = 200
+
+ for i, (t, lat) in enumerate(zip(times, latencies, strict=False)):
+ x = 1.5 + i * 2.0
+ ax.text(x, 0.1, t, fontsize=FS, ha="center")
+
+ # Bar proportional to latency
+ bar_h = lat / 100.0
+ is_breach = lat > sla
+ fill = "#F8D7DA" if is_breach else GRAY1
+ edge = "#C62828" if is_breach else LN
+ draw_box(
+ ax,
+ x - 0.5,
+ 1.0,
+ 1.0,
+ bar_h,
+ "",
+ fill=fill,
+ rounded=False,
+ edgecolor=edge,
+ lw=1.5,
+ )
+ ax.text(
+ x,
+ 1.0 + bar_h + 0.15,
+ f"{lat}ms",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ color="#C62828" if is_breach else LN,
+ )
+
+ # Status
+ status = "ALERT!" if is_breach else "OK"
+ ax.text(
+ x,
+ 1.0 + bar_h + 0.55,
+ status,
+ fontsize=FS_SMALL,
+ ha="center",
+ fontweight="bold",
+ color="#C62828" if is_breach else "#2E7D32",
+ )
+
+ # SLA line
+ sla_y = 1.0 + sla / 100.0
+ ax.plot([0.8, 11.2], [sla_y, sla_y], "k--", lw=1.5)
+ ax.text(11.3, sla_y, f"SLA={sla}ms", fontsize=FS, va="center", fontweight="bold")
+
+ # Sliding window bracket
+ ax.annotate(
+ "",
+ xy=(1.0, 5.3),
+ xytext=(5.0, 5.3),
+ arrowprops={"arrowstyle": "<->", "lw": 1.5, "color": LN},
+ )
+ ax.text(3.0, 5.6, "okno = 5 min", fontsize=FS, ha="center", fontweight="bold")
+
+ ax.annotate(
+ "",
+ xy=(3.0, 4.8),
+ xytext=(5.0, 4.8),
+ arrowprops={"arrowstyle": "<->", "lw": 1, "color": "#555"},
+ )
+ ax.text(
+ 4.0,
+ 4.4,
+ "krok = 1 min\n(nakładanie!)",
+ fontsize=FS_SMALL,
+ ha="center",
+ style="italic",
+ )
+
+ save_fig(fig, "q20_sliding_sla.png")
+
+
+# ============================================================
+# 6. Session window — user sessions
+# ============================================================
+def gen_session_users() -> None:
+ """Gen session users."""
+ fig, axes = plt.subplots(2, 1, figsize=(10, 5))
+ fig.suptitle(
+ "Session Window — sesje użytkowników (gap = 30 min)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ )
+
+ # Anna: 2 sessions
+ ax = axes[0]
+ ax.set_xlim(0, 14)
+ ax.set_ylim(0, 3.5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("Użytkownik Anna", fontsize=FS_LABEL, fontweight="bold")
+
+ ax.annotate(
+ "",
+ xy=(13.5, 1.0),
+ xytext=(0.3, 1.0),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+
+ # Clicks cluster 1
+ for x in [1.0, 1.8, 2.5, 3.2]:
+ ax.plot(x, 1.0, "ko", markersize=6)
+ # Clicks cluster 2
+ for x in [9.0, 9.8, 10.5]:
+ ax.plot(x, 1.0, "ko", markersize=6)
+
+ # Sessions
+ rect1 = mpatches.FancyBboxPatch(
+ (0.7, 1.5),
+ 2.8,
+ 1.2,
+ boxstyle="round,pad=0.1",
+ facecolor=GRAY1,
+ edgecolor=LN,
+ lw=1.5,
+ )
+ ax.add_patch(rect1)
+ ax.text(
+ 2.1,
+ 2.1,
+ "Sesja 1\n4 kliknięcia, 12 min",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ )
+
+ rect2 = mpatches.FancyBboxPatch(
+ (8.7, 1.5),
+ 2.1,
+ 1.2,
+ boxstyle="round,pad=0.1",
+ facecolor=GRAY3,
+ edgecolor=LN,
+ lw=1.5,
+ )
+ ax.add_patch(rect2)
+ ax.text(
+ 9.75,
+ 2.1,
+ "Sesja 2\n3 kliknięcia, 8 min",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ )
+
+ # Gap
+ ax.annotate(
+ "",
+ xy=(8.5, 0.5),
+ xytext=(3.8, 0.5),
+ arrowprops={"arrowstyle": "<->", "lw": 1.5, "color": LN},
+ )
+ ax.text(
+ 6.15,
+ 0.1,
+ "cisza 45 min > gap(30)",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ style="italic",
+ )
+
+ # Bob: 1 session
+ ax = axes[1]
+ ax.set_xlim(0, 14)
+ ax.set_ylim(0, 3.5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("Użytkownik Bob", fontsize=FS_LABEL, fontweight="bold")
+
+ ax.annotate(
+ "",
+ xy=(13.5, 1.0),
+ xytext=(0.3, 1.0),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+
+ # Clicks spread evenly
+ bobs = [1.0, 2.5, 4.0, 5.5, 7.0, 8.5, 10.0]
+ for x in bobs:
+ ax.plot(x, 1.0, "ko", markersize=6)
+
+ rect = mpatches.FancyBboxPatch(
+ (0.7, 1.5),
+ 9.6,
+ 1.2,
+ boxstyle="round,pad=0.1",
+ facecolor=GRAY1,
+ edgecolor=LN,
+ lw=2,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 5.5,
+ 2.1,
+ "Sesja 1 (ciągła) — 7 kliknięć, każde < 30 min od poprzedniego",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.92])
+ save_fig(fig, "q20_session_users.png")
+
+
+# ============================================================
+# 7. Streaming ecosystem overview
+# ============================================================
+def gen_streaming_ecosystem() -> None:
+ """Gen streaming ecosystem."""
+ fig, ax = plt.subplots(figsize=(10, 5.5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Ekosystem przetwarzania strumieniowego",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Source
+ draw_box(
+ ax,
+ 0.3,
+ 2.5,
+ 2.0,
+ 3.0,
+ "Kafka\nTopics\n(źródło)",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Engines
+ engines = [
+ ("Kafka Streams\n(library w JVM)", GRAY1, 4.7),
+ ("Apache Flink\n(klaster)", GRAY3, 3.2),
+ ("Spark Streaming\n(klaster)", GRAY5, 1.7),
+ ]
+ for label, color, y in engines:
+ draw_box(
+ ax, 4.0, y, 3.0, 1.2, label, fill=color, fontsize=FS, fontweight="bold"
+ )
+ draw_arrow(ax, 2.3, 4.0, 4.0, y + 0.6, lw=1.5)
+
+ # Sinks
+ sinks = [
+ ("Kafka topic\n/ baza danych", GRAY4, 4.7),
+ ("DB / Kafka\n/ S3", GRAY4, 3.2),
+ ("HDFS / DB\n/ dashboard", GRAY4, 1.7),
+ ]
+ for label, color, y in sinks:
+ draw_box(ax, 8.5, y, 2.5, 1.2, label, fill=color, fontsize=FS)
+ draw_arrow(ax, 7.0, y + 0.6, 8.5, y + 0.6, lw=1.5)
+
+ # Labels
+ ax.text(1.3, 6.0, "ŹRÓDŁO", fontsize=FS_LABEL, ha="center", fontweight="bold")
+ ax.text(5.5, 6.2, "SILNIK", fontsize=FS_LABEL, ha="center", fontweight="bold")
+ ax.text(9.75, 6.2, "WYNIK", fontsize=FS_LABEL, ha="center", fontweight="bold")
+
+ # Latency annotations
+ ax.text(5.5, 5.95, "~1-10 ms", fontsize=FS_SMALL, ha="center", style="italic")
+ ax.text(5.5, 4.5, "<10 ms", fontsize=FS_SMALL, ha="center", style="italic")
+ ax.text(5.5, 3.0, "~100 ms", fontsize=FS_SMALL, ha="center", style="italic")
+
+ save_fig(fig, "q20_streaming_ecosystem.png")
+
+
+# ============================================================
+# 8. True streaming vs Micro-batch
+# ============================================================
+def gen_true_vs_microbatch() -> None:
+ """Gen true vs microbatch."""
+ fig, axes = plt.subplots(2, 1, figsize=(10, 5.5))
+ fig.suptitle("True Streaming vs Micro-Batch", fontsize=FS_TITLE, fontweight="bold")
+
+ # True streaming
+ ax = axes[0]
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 3.5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "TRUE STREAMING (Flink, Kafka Streams) — event-by-event",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ )
+
+ for i in range(6):
+ x = 1.0 + i * 1.8
+ # Event
+ draw_box(
+ ax,
+ x,
+ 2.0,
+ 0.8,
+ 0.7,
+ f"e{i + 1}",
+ fill=GRAY1,
+ fontsize=FS,
+ fontweight="bold",
+ rounded=False,
+ )
+ # Arrow down
+ draw_arrow(ax, x + 0.4, 2.0, x + 0.4, 1.4, lw=1)
+ # Result
+ draw_box(
+ ax,
+ x,
+ 0.5,
+ 0.8,
+ 0.7,
+ f"r{i + 1}",
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ rounded=False,
+ )
+ # Latency label
+ ax.text(x + 0.4, 1.6, "~ms", fontsize=5, ha="center", color="#555")
+
+ ax.text(
+ 11.5,
+ 1.3,
+ "Latencja:\n< 10 ms",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ # Micro-batch
+ ax = axes[1]
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 3.5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "MICRO-BATCH (Spark Streaming) — grupami co ~100ms",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ )
+
+ batch_colors = [GRAY1, GRAY2, GRAY3]
+ for b in range(3):
+ bx = 0.8 + b * 3.5
+ # Batch boundary
+ draw_box(ax, bx, 1.8, 3.0, 1.0, "", fill=batch_colors[b], rounded=True, lw=1.5)
+ ax.text(
+ bx + 1.5, 2.6, f"Batch {b + 1}", fontsize=FS, ha="center", fontweight="bold"
+ )
+ for j in range(3):
+ ex = bx + 0.3 + j * 0.9
+ draw_box(
+ ax,
+ ex,
+ 2.0,
+ 0.7,
+ 0.5,
+ f"e{b * 3 + j + 1}",
+ fill="white",
+ fontsize=FS_SMALL,
+ rounded=False,
+ )
+
+ # Arrow down
+ draw_arrow(ax, bx + 1.5, 1.8, bx + 1.5, 1.2, lw=1.5)
+ # Result
+ draw_box(
+ ax,
+ bx + 0.5,
+ 0.4,
+ 2.0,
+ 0.7,
+ f"result {b + 1}",
+ fill=GRAY4,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ ax.text(
+ 11.5,
+ 1.3,
+ "Latencja:\n~100ms-s",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.92])
+ save_fig(fig, "q20_true_vs_microbatch.png")
+
+
+# ============================================================
+# 9. Platform comparison table
+# ============================================================
+def gen_platform_comparison() -> None:
+ """Gen platform comparison."""
+ fig, ax = plt.subplots(figsize=(9, 5))
+ ax.set_xlim(0, 11.5)
+ ax.set_ylim(-6, 1)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Porównanie platform strumieniowych",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ headers = ["Cecha", "Kafka Streams", "Apache Flink", "Spark Streaming"]
+ col_w = [2.5, 2.8, 2.8, 2.8]
+ rows = [
+ ["Model", "event-by-event", "event-by-event", "micro-batch (~100ms)"],
+ ["Deployment", "library (w JVM)", "klaster", "klaster"],
+ ["Latencja", "~1-10 ms", "< 10 ms", "100 ms - sekundy"],
+ ["Exactly-once", "Kafka TXN", "checkpointing", "WAL"],
+ ["State", "RocksDB local", "RocksDB + ckpt", "in-memory / ext"],
+ ["Okna", "T, S, Session", "wszystkie + custom", "T, S"],
+ ["Use case", "Kafka → Kafka", "złożona analityka", "ETL + ML / SQL"],
+ ]
+ draw_table(
+ ax,
+ headers,
+ rows,
+ x0=0.25,
+ y0=0.5,
+ col_widths=col_w,
+ row_h=0.6,
+ fontsize=7,
+ header_fontsize=8,
+ )
+
+ save_fig(fig, "q20_platform_comparison.png")
+
+
+# ============================================================
+# 10. Kafka Streams architecture
+# ============================================================
+def gen_kafka_streams_arch() -> None:
+ """Gen kafka streams arch."""
+ fig, ax = plt.subplots(figsize=(9, 5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Kafka Streams — architektura (library w JVM)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Outer box: Your Java application
+ draw_box(ax, 0.5, 0.5, 11.0, 5.5, "", fill=GRAY4, rounded=True, lw=2.5)
+ ax.text(
+ 6.0,
+ 5.7,
+ "Twoja aplikacja Java (JVM)",
+ fontsize=FS_LABEL,
+ ha="center",
+ fontweight="bold",
+ )
+
+ # Kafka Consumer
+ draw_box(
+ ax,
+ 1.0,
+ 3.0,
+ 2.5,
+ 1.5,
+ "Kafka\nConsumer\n(input topic)",
+ fill=GRAY1,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Processing
+ draw_box(
+ ax,
+ 4.5,
+ 3.0,
+ 2.5,
+ 1.5,
+ "Kafka Streams\n(logika\nbiznesowa)",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Kafka Producer
+ draw_box(
+ ax,
+ 8.0,
+ 3.0,
+ 2.5,
+ 1.5,
+ "Kafka\nProducer\n(output topic)",
+ fill=GRAY1,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Arrows
+ draw_arrow(ax, 3.5, 3.75, 4.5, 3.75, lw=2)
+ draw_arrow(ax, 7.0, 3.75, 8.0, 3.75, lw=2)
+
+ # RocksDB state store
+ draw_box(
+ ax,
+ 4.5,
+ 1.0,
+ 2.5,
+ 1.3,
+ "RocksDB\n(stan lokalny)",
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.plot([5.75, 5.75], [3.0, 2.3], color=LN, lw=1.5)
+ ax.text(
+ 7.3,
+ 1.6,
+ "okna, joiny,\nagregacje",
+ fontsize=FS_SMALL,
+ style="italic",
+ va="center",
+ )
+
+ # Key message
+ ax.text(
+ 6.0,
+ 0.2,
+ "NIE potrzebujesz osobnego klastra! Skalujesz = więcej instancji JVM.",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": "white", "edgecolor": LN},
+ )
+
+ save_fig(fig, "q20_kafka_streams_arch.png")
+
+
+# ============================================================
+# 11. Flink architecture + checkpointing
+# ============================================================
+def gen_flink_arch() -> None:
+ """Gen flink arch."""
+ fig, ax = plt.subplots(figsize=(9, 6))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 8)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Apache Flink — architektura klastra + checkpointing",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Cluster border
+ draw_box(ax, 0.3, 1.0, 11.4, 6.2, "", fill=GRAY4, rounded=True, lw=2.5)
+ ax.text(
+ 6.0, 6.95, "FLINK CLUSTER", fontsize=FS_LABEL, ha="center", fontweight="bold"
+ )
+
+ # Job Manager
+ draw_box(
+ ax,
+ 1.0,
+ 5.5,
+ 3.0,
+ 1.2,
+ "Job Manager\n(koordynacja,\ncheckpointy)",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Task Managers
+ draw_box(ax, 1.0, 3.0, 10.0, 2.0, "", fill="white", rounded=True, lw=1.5)
+ ax.text(
+ 6.0, 4.7, "Task Managers (workery)", fontsize=FS, ha="center", fontweight="bold"
+ )
+
+ slots = ["source\n& map()", "map()", "window()\n& reduce", "sink()"]
+ for i, s in enumerate(slots):
+ x = 1.5 + i * 2.4
+ draw_box(
+ ax,
+ x,
+ 3.3,
+ 2.0,
+ 1.2,
+ f"Slot {i + 1}\n{s}",
+ fill=GRAY1,
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+
+ draw_arrow(ax, 2.5, 5.5, 6.0, 5.0, lw=1.5, style="->")
+ ax.text(5.0, 5.5, "przydziela\npodzadania", fontsize=FS_SMALL, style="italic")
+
+ # Checkpoint storage
+ draw_box(
+ ax,
+ 5.5,
+ 1.2,
+ 3.5,
+ 1.2,
+ "Checkpoint Storage\n(HDFS / S3)",
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.plot([7.25, 7.25], [2.4, 3.3], color=LN, lw=1.5, linestyle="--")
+ ax.text(8.0, 2.7, "snapshoty\nstanu", fontsize=FS_SMALL, style="italic")
+
+ # Barrier concept at bottom
+ ax.text(3.0, 1.6, "Barrier:", fontsize=FS, fontweight="bold")
+ barrier_boxes = ["source", "|B|", "map", "|B|", "sink"]
+ bx = 0.8
+ for _i, b in enumerate(barrier_boxes):
+ if b == "|B|":
+ ax.text(
+ bx + 0.3,
+ 1.5,
+ b,
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.1", "facecolor": GRAY5, "edgecolor": LN},
+ )
+ draw_arrow(ax, bx, 1.5, bx + 0.1, 1.5, lw=1)
+ bx += 0.7
+ else:
+ draw_box(
+ ax,
+ bx,
+ 1.3,
+ 1.0,
+ 0.45,
+ b,
+ fill=GRAY1,
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ bx += 1.2
+
+ save_fig(fig, "q20_flink_arch.png")
+
+
+# ============================================================
+# 12. Spark Streaming architecture
+# ============================================================
+def gen_spark_streaming_arch() -> None:
+ """Gen spark streaming arch."""
+ fig, ax = plt.subplots(figsize=(9, 5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Spark Streaming — architektura (micro-batch)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Cluster border
+ draw_box(ax, 0.3, 0.5, 11.4, 5.8, "", fill=GRAY4, rounded=True, lw=2.5)
+ ax.text(
+ 6.0, 6.0, "SPARK CLUSTER", fontsize=FS_LABEL, ha="center", fontweight="bold"
+ )
+
+ # Driver
+ draw_box(
+ ax,
+ 1.0,
+ 4.5,
+ 3.0,
+ 1.2,
+ "Driver\n(planuje mini-batche)",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ draw_arrow(ax, 2.5, 4.5, 6.0, 4.0, lw=1.5)
+
+ # Batches
+ batches = ["batch 1\n(e1,e2,e3)", "batch 2\n(e4,e5,e6)", "batch 3\n(e7,e8,e9)"]
+ for i, b in enumerate(batches):
+ y = 2.8 - i * 1.0
+ draw_box(
+ ax, 4.5, y, 2.5, 0.8, b, fill=GRAY1, fontsize=FS_SMALL, fontweight="bold"
+ )
+ # map → reduce
+ draw_arrow(ax, 7.0, y + 0.4, 7.5, y + 0.4, lw=1)
+ draw_box(ax, 7.5, y, 1.3, 0.8, "map→\nreduce", fill=GRAY3, fontsize=5.5)
+ draw_arrow(ax, 8.8, y + 0.4, 9.3, y + 0.4, lw=1)
+ draw_box(
+ ax, 9.3, y, 1.5, 0.8, f"result {i + 1}", fill="white", fontsize=FS_SMALL
+ )
+
+ # Spark ecosystem
+ draw_box(
+ ax,
+ 1.0,
+ 1.0,
+ 3.0,
+ 1.0,
+ "Spark SQL / MLlib\n(ten sam ekosystem!)",
+ fill=GRAY5,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ ax.text(
+ 6.0,
+ 0.3,
+ "ZALETA: batch API | WADA: latencja ≥ batch interval (~100ms)",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": "white", "edgecolor": LN},
+ )
+
+ save_fig(fig, "q20_spark_streaming_arch.png")
+
+
+# ============================================================
+# 13. Lambda vs Kappa architecture
+# ============================================================
+def gen_lambda_vs_kappa() -> None:
+ """Gen lambda vs kappa."""
+ fig, axes = plt.subplots(2, 1, figsize=(10, 7))
+ fig.suptitle("Architektura Lambda vs Kappa", fontsize=FS_TITLE, fontweight="bold")
+
+ # --- Lambda ---
+ ax = axes[0]
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "LAMBDA — 2 ścieżki (batch + speed)", fontsize=FS_LABEL, fontweight="bold"
+ )
+
+ # Source
+ draw_box(
+ ax,
+ 0.3,
+ 1.8,
+ 2.0,
+ 1.5,
+ "Źródło\ndanych",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Batch layer (top)
+ draw_box(
+ ax,
+ 3.5,
+ 3.3,
+ 3.0,
+ 1.2,
+ "Batch Layer\n(Spark)\nprzelicza co godzinę",
+ fill=GRAY1,
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 2.3, 3.0, 3.5, 3.9, lw=1.5)
+
+ # Speed layer (bottom)
+ draw_box(
+ ax,
+ 3.5,
+ 0.8,
+ 3.0,
+ 1.2,
+ "Speed Layer\n(Flink)\nreal-time",
+ fill=GRAY3,
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 2.3, 2.2, 3.5, 1.4, lw=1.5)
+
+ # Results
+ draw_box(
+ ax,
+ 7.5,
+ 3.3,
+ 2.0,
+ 1.2,
+ "Dokładne\nwyniki\n(wolne)",
+ fill=GRAY4,
+ fontsize=FS_SMALL,
+ )
+ draw_arrow(ax, 6.5, 3.9, 7.5, 3.9, lw=1.5)
+
+ draw_box(
+ ax,
+ 7.5,
+ 0.8,
+ 2.0,
+ 1.2,
+ "Przybliżone\nwyniki\n(szybkie)",
+ fill=GRAY4,
+ fontsize=FS_SMALL,
+ )
+ draw_arrow(ax, 6.5, 1.4, 7.5, 1.4, lw=1.5)
+
+ # Merge
+ draw_box(
+ ax,
+ 10.0,
+ 2.0,
+ 1.5,
+ 1.5,
+ "MERGE\n→ UI",
+ fill=GRAY5,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 9.5, 3.5, 10.0, 3.0, lw=1.5)
+ draw_arrow(ax, 9.5, 1.8, 10.0, 2.5, lw=1.5)
+
+ ax.text(
+ 6.0,
+ 0.1,
+ "2 systemy, 2 kody — złożone ale pewne",
+ fontsize=FS,
+ ha="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ # --- Kappa ---
+ ax = axes[1]
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 4)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "KAPPA — 1 ścieżka (streaming only)", fontsize=FS_LABEL, fontweight="bold"
+ )
+
+ # Source
+ draw_box(
+ ax,
+ 0.3,
+ 1.3,
+ 2.0,
+ 1.5,
+ "Źródło\ndanych",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Single streaming layer
+ draw_box(
+ ax,
+ 3.5,
+ 1.3,
+ 3.5,
+ 1.5,
+ "Streaming Layer\n(Flink)\n+ replay z Kafka log",
+ fill=GRAY1,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 2.3, 2.05, 3.5, 2.05, lw=2)
+
+ # Output
+ draw_box(
+ ax,
+ 8.0,
+ 1.3,
+ 2.5,
+ 1.5,
+ "Wyniki\n→ UI",
+ fill=GRAY4,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 7.0, 2.05, 8.0, 2.05, lw=2)
+
+ # Replay arrow
+ ax.annotate(
+ "",
+ xy=(3.5, 1.0),
+ xytext=(7.0, 1.0),
+ arrowprops={
+ "arrowstyle": "<-",
+ "lw": 1.5,
+ "color": LN,
+ "connectionstyle": "arc3,rad=0.3",
+ "linestyle": "--",
+ },
+ )
+ ax.text(
+ 5.25,
+ 0.3,
+ "Replay z Kafka\n(przetwórz historię od nowa)",
+ fontsize=FS_SMALL,
+ ha="center",
+ style="italic",
+ )
+
+ ax.text(
+ 6.0,
+ 3.3,
+ "1 system, 1 kod — prostsze, ale replay = dużo I/O",
+ fontsize=FS,
+ ha="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.92])
+ save_fig(fig, "q20_lambda_vs_kappa.png")
+
+
+# ============================================================
+# 14. Lambda vs Kappa comparison table
+# ============================================================
+def gen_lambda_kappa_table() -> None:
+ """Gen lambda kappa table."""
+ fig, ax = plt.subplots(figsize=(8, 3.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(-4.5, 1)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Lambda vs Kappa — porównanie", fontsize=FS_TITLE, fontweight="bold", pad=10
+ )
+
+ headers = ["Cecha", "Lambda", "Kappa"]
+ col_w = [2.5, 3.5, 3.5]
+ rows = [
+ ["Ścieżki", "2 (batch + speed)", "1 (streaming)"],
+ ["Kod", "2 implementacje", "1 implementacja"],
+ ["Złożoność", "wysoka", "niska"],
+ ["Replay", "batch przelicza", "Kafka replay"],
+ ["Spójność", "merge wymagany", "natywna"],
+ ["Przykład", "Netflix, LinkedIn", "Uber, Confluent"],
+ ]
+ draw_table(
+ ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.55, fontsize=7.5
+ )
+
+ save_fig(fig, "q20_lambda_kappa_table.png")
+
+
+# ============================================================
+# 15. Exactly-once comparison
+# ============================================================
+def gen_exactly_once() -> None:
+ """Gen exactly once."""
+ fig, ax = plt.subplots(figsize=(9, 5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Exactly-Once — mechanizmy na 3 platformach",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Flink
+ draw_box(ax, 0.3, 4.3, 11.0, 2.0, "", fill=GRAY4, rounded=True, lw=1.5)
+ ax.text(
+ 1.0,
+ 5.9,
+ "Flink — Distributed Snapshots (Chandy-Lamport)",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ flink_steps = ["source", "|B|", "map()", "|B|", "sink()"]
+ bx = 1.0
+ for s in flink_steps:
+ if s == "|B|":
+ ax.text(
+ bx + 0.25,
+ 4.85,
+ s,
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.1", "facecolor": GRAY5, "edgecolor": LN},
+ )
+ draw_arrow(ax, bx - 0.1, 4.85, bx + 0.05, 4.85, lw=1)
+ bx += 0.7
+ else:
+ draw_box(
+ ax,
+ bx,
+ 4.6,
+ 1.5,
+ 0.55,
+ s,
+ fill=GRAY1,
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ bx += 1.8
+ ax.text(
+ 8.5,
+ 5.0,
+ "barrier → save state\n→ checkpoint (HDFS/S3)",
+ fontsize=FS_SMALL,
+ style="italic",
+ )
+
+ # Kafka Streams
+ draw_box(ax, 0.3, 2.3, 11.0, 1.5, "", fill=GRAY1, rounded=True, lw=1.5)
+ ax.text(
+ 1.0, 3.5, "Kafka Streams — Transakcje Kafka", fontsize=FS, fontweight="bold"
+ )
+ ax.text(
+ 1.5,
+ 2.85,
+ "idempotent producer + begin TX → produce → commit TX → consumer offsets w TX",
+ fontsize=FS_SMALL,
+ )
+
+ # Spark
+ draw_box(ax, 0.3, 0.5, 11.0, 1.5, "", fill=GRAY3, rounded=True, lw=1.5)
+ ax.text(
+ 1.0,
+ 1.7,
+ "Spark Streaming — Write-Ahead Log (WAL)",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.text(
+ 1.5,
+ 1.05,
+ "WAL + checkpointing micro-batchów + idempotent sinks (np. upsert do DB)",
+ fontsize=FS_SMALL,
+ )
+
+ save_fig(fig, "q20_exactly_once.png")
+
+
+# ============================================================
+# 16. Late data strategies (DRAS)
+# ============================================================
+def gen_late_data_strategies() -> None:
+ """Gen late data strategies."""
+ fig, ax = plt.subplots(figsize=(9, 5.5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Late Data — 4 strategie (mnemonik DRAS)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Setup: window closed, late event arrives
+ draw_box(
+ ax,
+ 0.5,
+ 5.5,
+ 4.5,
+ 1.0,
+ "Okno [14:00-14:05]\nZAMKNIĘTE o 14:05",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_box(
+ ax,
+ 6.0,
+ 5.5,
+ 4.5,
+ 1.0,
+ "Spóźnione zdarzenie\nevent_time=14:00:03\narrives=14:05:30",
+ fill="#F8D7DA",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 10.5, 6.0, 5.0, 6.0, lw=2, color="#C62828", style="->")
+ ax.text(
+ 7.5,
+ 5.2,
+ "LATE!",
+ fontsize=FS_LABEL,
+ ha="center",
+ fontweight="bold",
+ color="#C62828",
+ )
+
+ # 4 strategies
+ strategies = [
+ ("D — Drop", "Odrzuć spóźnione", "/dev/null", GRAY4),
+ ("R — Recompute", "Przelicz okno ponownie", "poprawne ale kosztowne", GRAY1),
+ (
+ "A — Allowed lateness",
+ "Czekaj dodatkowy czas\n(np. +2 min)",
+ "kompromis pamięci",
+ GRAY2,
+ ),
+ (
+ "S — Side output",
+ "Przekieruj do osobnej\nkolejki",
+ "elastyczne, ręczna analiza",
+ GRAY3,
+ ),
+ ]
+ for i, (name, desc, tradeoff, color) in enumerate(strategies):
+ y = 3.8 - i * 1.1
+ draw_box(ax, 0.5, y, 2.5, 0.9, name, fill=color, fontsize=FS, fontweight="bold")
+ ax.text(3.3, y + 0.45, desc, fontsize=FS_SMALL, va="center")
+ ax.text(
+ 8.5,
+ y + 0.45,
+ tradeoff,
+ fontsize=FS_SMALL,
+ va="center",
+ style="italic",
+ color="#555",
+ )
+
+ save_fig(fig, "q20_late_data_strategies.png")
+
+
+# ============================================================
+# 17. Decision tree — which platform
+# ============================================================
+def gen_decision_tree() -> None:
+ """Gen decision tree."""
+ fig, ax = plt.subplots(figsize=(10, 5.5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Drzewo decyzyjne — wybór platformy",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Root question
+ draw_box(
+ ax,
+ 3.5,
+ 5.5,
+ 4.5,
+ 1.0,
+ "Latencja < 10ms\nwymagana?",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # TAK branch
+ draw_arrow(ax, 3.5, 5.7, 2.0, 5.0, lw=1.5)
+ ax.text(2.3, 5.3, "TAK", fontsize=FS, fontweight="bold")
+
+ draw_box(
+ ax,
+ 0.3,
+ 3.5,
+ 3.5,
+ 1.0,
+ "Dane już w Kafce?\nProste transformacje?",
+ fill=GRAY1,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # TAK → Kafka Streams
+ draw_arrow(ax, 0.3, 3.7, -0.1, 3.0, lw=1.5)
+ ax.text(0.0, 3.3, "TAK", fontsize=FS_SMALL, fontweight="bold")
+ draw_box(
+ ax,
+ -0.3,
+ 1.8,
+ 2.5,
+ 1.0,
+ "Kafka\nStreams",
+ fill=GRAY5,
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ )
+
+ # NIE → Flink
+ draw_arrow(ax, 3.8, 3.7, 4.5, 3.0, lw=1.5)
+ ax.text(4.0, 3.3, "NIE\n(złożona logika)", fontsize=FS_SMALL)
+ draw_box(
+ ax,
+ 3.0,
+ 1.8,
+ 2.5,
+ 1.0,
+ "Apache\nFlink",
+ fill=GRAY5,
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ )
+
+ # NIE branch
+ draw_arrow(ax, 8.0, 5.7, 9.5, 5.0, lw=1.5)
+ ax.text(8.7, 5.3, "NIE", fontsize=FS, fontweight="bold")
+
+ draw_box(
+ ax,
+ 7.5,
+ 3.5,
+ 4.2,
+ 1.0,
+ "~100ms-1s OK?\nPotrzeba ML / SQL?",
+ fill=GRAY1,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # TAK + ML → Spark
+ draw_arrow(ax, 9.5, 3.5, 9.5, 3.0, lw=1.5)
+ ax.text(10.0, 3.3, "TAK + ML/SQL", fontsize=FS_SMALL)
+ draw_box(
+ ax,
+ 8.0,
+ 1.8,
+ 2.5,
+ 1.0,
+ "Spark\nStreaming",
+ fill=GRAY5,
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ )
+
+ # TAK + proste → Kafka Streams too
+ draw_arrow(ax, 7.5, 3.7, 6.5, 3.0, lw=1.5)
+ ax.text(6.3, 3.3, "proste + TAK", fontsize=FS_SMALL)
+ draw_box(
+ ax,
+ 5.8,
+ 1.8,
+ 2.0,
+ 1.0,
+ "Kafka\nStreams",
+ fill=GRAY5,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Legend
+ ax.text(
+ 6.0,
+ 0.7,
+ "Reguła: Kafka Streams = najprostsze (library) | "
+ "Flink = najpotężniejszy (true streaming) | Spark = ekosystem ML",
+ fontsize=FS,
+ ha="center",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ save_fig(fig, "q20_decision_tree.png")
+
+
+# ============================================================
+# MAIN
+# ============================================================
+if __name__ == "__main__":
+ print("Generating ALL PYTANIE 20 diagrams...")
+ gen_batch_vs_streaming()
+ gen_window_types()
+ gen_event_vs_processing_time()
+ gen_tumbling_fraud()
+ gen_sliding_sla()
+ gen_session_users()
+ gen_streaming_ecosystem()
+ gen_true_vs_microbatch()
+ gen_platform_comparison()
+ gen_kafka_streams_arch()
+ gen_flink_arch()
+ gen_spark_streaming_arch()
+ gen_lambda_vs_kappa()
+ gen_lambda_kappa_table()
+ gen_exactly_once()
+ gen_late_data_strategies()
+ gen_decision_tree()
+ print("\nAll 17 PYTANIE 20 diagrams generated successfully!")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_q23_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_q23_diagrams.py
new file mode 100755
index 0000000..edfb9be
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_q23_diagrams.py
@@ -0,0 +1,2722 @@
+#!/usr/bin/env python3
+"""Generate all diagrams for PYTANIE 23: Segmentacja obrazu.
+
+A4-compatible, monochrome-friendly (grays + one accent), 300 DPI.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+from matplotlib import patches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+import numpy as np
+
+rng = np.random.default_rng(42)
+
+DPI = 300
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+# Color palette — monochrome-friendly
+BLACK = "#000000"
+WHITE = "#FFFFFF"
+GRAY1 = "#F5F5F5"
+GRAY2 = "#E0E0E0"
+GRAY3 = "#BDBDBD"
+GRAY4 = "#9E9E9E"
+GRAY5 = "#757575"
+GRAY6 = "#424242"
+ACCENT = "#4A90D9" # single blue accent for highlights
+ACCENT_LIGHT = "#B3D4FC"
+RED_ACCENT = "#D32F2F"
+GREEN_ACCENT = "#388E3C"
+
+FS = 9
+FS_TITLE = 11
+FS_SMALL = 7
+FS_TINY = 6
+
+
+# ============================================================
+# 1. OTSU — Bimodal histogram + within-class variance
+# ============================================================
+def generate_otsu_bimodal() -> None:
+ """Generate otsu bimodal."""
+ _fig, axes = plt.subplots(1, 3, figsize=(11, 3.5))
+
+ # --- Panel 1: Bimodal histogram ---
+ ax = axes[0]
+ dark = rng.normal(60, 20, 3000).clip(0, 255)
+ bright = rng.normal(190, 25, 2000).clip(0, 255)
+ all_pixels = np.concatenate([dark, bright])
+
+ counts, _bins, _bars = ax.hist(
+ all_pixels, bins=64, color=GRAY3, edgecolor=GRAY5, linewidth=0.5
+ )
+ ax.axvline(
+ x=128, color=RED_ACCENT, linewidth=2, linestyle="--", label="Próg Otsu T=128"
+ )
+ ax.fill_betweenx([0, max(counts) * 1.1], 0, 128, alpha=0.12, color=ACCENT)
+ ax.fill_betweenx([0, max(counts) * 1.1], 128, 255, alpha=0.12, color=RED_ACCENT)
+ ax.text(
+ 45,
+ max(counts) * 0.85,
+ "Klasa 0\n(tło)",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ color=ACCENT,
+ )
+ ax.text(
+ 195,
+ max(counts) * 0.85,
+ "Klasa 1\n(obiekt)",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ color=RED_ACCENT,
+ )
+ ax.annotate(
+ "Garb 1",
+ xy=(60, max(counts) * 0.6),
+ fontsize=FS_SMALL,
+ ha="center",
+ arrowprops={"arrowstyle": "->", "color": GRAY5},
+ xytext=(30, max(counts) * 0.45),
+ )
+ ax.annotate(
+ "Garb 2",
+ xy=(190, max(counts) * 0.5),
+ fontsize=FS_SMALL,
+ ha="center",
+ arrowprops={"arrowstyle": "->", "color": GRAY5},
+ xytext=(220, max(counts) * 0.35),
+ )
+ ax.set_xlabel("Jasność piksela (0-255)", fontsize=FS)
+ ax.set_ylabel("Liczba pikseli", fontsize=FS)
+ ax.set_title("Histogram bimodalny", fontsize=FS_TITLE, fontweight="bold")
+ ax.legend(fontsize=FS_SMALL, loc="upper right")
+ ax.set_xlim(0, 255)
+
+ # --- Panel 2: Within-class variance explanation ---
+ ax = axes[1]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title("Wariancja wewnątrzklasowa", fontsize=FS_TITLE, fontweight="bold")
+
+ y = 9.2
+ texts = [
+ (
+ "Wariancja = jak bardzo wartości\nróżnią się od średniej",
+ FS,
+ "black",
+ "normal",
+ ),
+ ("", 0, "black", "normal"),
+ ("Klasa 0 (piksele ≤ T):", FS, ACCENT, "bold"),
+ (" wartości: 30, 50, 45, 60, 55", FS_SMALL, "black", "normal"),
+ (" średnia μ₀ = 48", FS_SMALL, "black", "normal"),
+ (" σ₀² = ((30-48)²+(50-48)²+...)/5 = 108", FS_SMALL, "black", "normal"),
+ ("", 0, "black", "normal"),
+ ("Klasa 1 (piksele > T):", FS, RED_ACCENT, "bold"),
+ (" wartości: 180, 200, 190, 210, 195", FS_SMALL, "black", "normal"),
+ (" średnia μ₁ = 195", FS_SMALL, "black", "normal"),
+ (" σ₁² = ((180-195)²+...)/5 = 100", FS_SMALL, "black", "normal"),
+ ("", 0, "black", "normal"),
+ ("σ²_wewnątrz = w₀·σ₀² + w₁·σ₁²", FS, BLACK, "bold"),
+ ("= 0.6·108 + 0.4·100 = 104.8", FS_SMALL, "black", "normal"),
+ ("", 0, "black", "normal"),
+ ("Otsu próbuje KAŻDE T: 0,1,...,255", FS_SMALL, GREEN_ACCENT, "bold"),
+ ("Wybiera T dające MINIMUM σ²_wewnątrz", FS_SMALL, GREEN_ACCENT, "bold"),
+ ]
+ for txt, size, color, weight in texts:
+ if txt == "":
+ y -= 0.25
+ continue
+ ax.text(
+ 0.3,
+ y,
+ txt,
+ fontsize=size,
+ color=color,
+ fontweight=weight,
+ va="top",
+ transform=ax.transAxes if False else None,
+ )
+ y -= 0.55
+
+ # --- Panel 3: Jednorodność explanation ---
+ ax = axes[2]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title('"Jednorodne" = małe σ²', fontsize=FS_TITLE, fontweight="bold")
+
+ # Draw two clusters
+ # Good separation
+ c0 = rng.normal(2, 0.4, 15)
+ c1 = rng.normal(7, 0.4, 15)
+ y_pos_0 = rng.uniform(6, 8, 15)
+ y_pos_1 = rng.uniform(6, 8, 15)
+ ax.scatter(c0, y_pos_0, c=ACCENT, s=30, zorder=5, label="Klasa 0")
+ ax.scatter(c1, y_pos_1, c=RED_ACCENT, s=30, zorder=5, label="Klasa 1")
+ ax.axvline(x=4.5, color=GREEN_ACCENT, linewidth=2, linestyle="--")
+ ax.text(
+ 4.5,
+ 8.8,
+ "T optymalny",
+ ha="center",
+ fontsize=FS_SMALL,
+ color=GREEN_ACCENT,
+ fontweight="bold",
+ )
+ ax.text(
+ 2, 5.3, "σ₀² mała\n(skupione)", ha="center", fontsize=FS_SMALL, color=ACCENT
+ )
+ ax.text(
+ 7, 5.3, "σ₁² mała\n(skupione)", ha="center", fontsize=FS_SMALL, color=RED_ACCENT
+ )
+ ax.text(
+ 5,
+ 4,
+ "→ σ²_wewnątrz MINIMALNA\n→ klasy JEDNORODNE\n→ dobra segmentacja!",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ color=GREEN_ACCENT,
+ )
+
+ # Bad separation
+ c0b = rng.normal(3.5, 1.5, 15)
+ c1b = rng.normal(6, 1.5, 15)
+ y_pos_0b = rng.uniform(1, 3, 15)
+ y_pos_1b = rng.uniform(1, 3, 15)
+ ax.scatter(c0b, y_pos_0b, c=ACCENT, s=30, marker="x", zorder=5)
+ ax.scatter(c1b, y_pos_1b, c=RED_ACCENT, s=30, marker="x", zorder=5)
+ ax.axvline(x=4.5, color=GRAY4, linewidth=1, linestyle=":", ymin=0, ymax=0.35)
+ ax.text(
+ 5,
+ 0.3,
+ "σ²_wewnątrz DUŻA → klasy mieszają się → zły próg",
+ ha="center",
+ fontsize=FS_SMALL,
+ color=GRAY5,
+ )
+
+ ax.legend(fontsize=FS_SMALL, loc="upper left")
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_otsu_bimodal.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_otsu_bimodal.png")
+
+
+# ============================================================
+# 2. WATERSHED — Topographic flooding (not ASCII!)
+# ============================================================
+def generate_watershed() -> None:
+ """Generate watershed."""
+ _fig, axes = plt.subplots(1, 3, figsize=(11, 3.8))
+
+ # --- Panel 1: Image as topographic surface ---
+ ax = axes[0]
+ x = np.linspace(0, 10, 200)
+ # Create a surface with two valleys and a ridge
+ surface = (
+ 3 * np.exp(-((x - 3) ** 2) / 1.5)
+ + 4 * np.exp(-((x - 7) ** 2) / 1.2)
+ + 0.5 * np.sin(x * 2)
+ + 1
+ )
+ # Invert: valleys at objects (dark), peaks at boundaries (bright)
+ surface_inv = 6 - surface + 1
+
+ ax.fill_between(x, 0, surface_inv, color=GRAY2, alpha=0.7)
+ ax.plot(x, surface_inv, color=BLACK, linewidth=1.5)
+
+ # Mark valleys
+ ax.annotate(
+ "Dolina 1\n(obiekt A)",
+ xy=(3, surface_inv[60]),
+ fontsize=FS_SMALL,
+ ha="center",
+ va="bottom",
+ arrowprops={"arrowstyle": "->", "color": ACCENT},
+ xytext=(1.5, 5.5),
+ )
+ ax.annotate(
+ "Dolina 2\n(obiekt B)",
+ xy=(7, surface_inv[140]),
+ fontsize=FS_SMALL,
+ ha="center",
+ va="bottom",
+ arrowprops={"arrowstyle": "->", "color": RED_ACCENT},
+ xytext=(8.5, 5.5),
+ )
+ # Mark ridge
+ ax.annotate(
+ "Grań\n(granica)",
+ xy=(5, surface_inv[100]),
+ fontsize=FS_SMALL,
+ ha="center",
+ va="bottom",
+ arrowprops={"arrowstyle": "->", "color": GREEN_ACCENT},
+ xytext=(5, 6.5),
+ )
+
+ ax.set_xlabel("Pozycja piksela", fontsize=FS)
+ ax.set_ylabel("Jasność (= wysokość)", fontsize=FS)
+ ax.set_title("Krok 1: obraz → teren", fontsize=FS_TITLE, fontweight="bold")
+ ax.set_ylim(0, 7)
+
+ # --- Panel 2: Flooding ---
+ ax = axes[1]
+ ax.fill_between(x, 0, surface_inv, color=GRAY2, alpha=0.7)
+ ax.plot(x, surface_inv, color=BLACK, linewidth=1.5)
+
+ # Water level
+ water_level = 3.2
+ (x < 5) & (surface_inv < water_level)
+ (x >= 5) & (surface_inv < water_level)
+
+ # Fill water in valley 1
+ x_v1 = x[(x > 1) & (x < 5)]
+ s_v1 = surface_inv[(x > 1) & (x < 5)]
+ ax.fill_between(
+ x_v1, s_v1, water_level, where=s_v1 < water_level, color=ACCENT_LIGHT, alpha=0.6
+ )
+ # Fill water in valley 2
+ x_v2 = x[(x > 5) & (x < 9)]
+ s_v2 = surface_inv[(x > 5) & (x < 9)]
+ ax.fill_between(
+ x_v2, s_v2, water_level, where=s_v2 < water_level, color="#FFCDD2", alpha=0.6
+ )
+
+ ax.axhline(y=water_level, color=ACCENT, linewidth=1, linestyle="--", alpha=0.5)
+ ax.text(3, 2.5, "Woda A", fontsize=FS, ha="center", color=ACCENT, fontweight="bold")
+ ax.text(
+ 7, 2.2, "Woda B", fontsize=FS, ha="center", color=RED_ACCENT, fontweight="bold"
+ )
+ ax.annotate(
+ "Tu się spotkają!\n→ GRANICA",
+ xy=(5, surface_inv[100]),
+ fontsize=FS_SMALL,
+ ha="center",
+ color=GREEN_ACCENT,
+ fontweight="bold",
+ arrowprops={"arrowstyle": "->", "color": GREEN_ACCENT},
+ xytext=(5, 6.2),
+ )
+
+ ax.set_xlabel("Pozycja piksela", fontsize=FS)
+ ax.set_title("Krok 2: zalewanie", fontsize=FS_TITLE, fontweight="bold")
+ ax.set_ylim(0, 7)
+
+ # --- Panel 3: Result with problem ---
+ ax = axes[2]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title("Krok 3: wynik", fontsize=FS_TITLE, fontweight="bold")
+
+ # Good result
+ rect1 = FancyBboxPatch(
+ (0.5, 6),
+ 3.5,
+ 3.2,
+ boxstyle="round,pad=0.1",
+ facecolor=ACCENT_LIGHT,
+ edgecolor=BLACK,
+ linewidth=1,
+ )
+ ax.add_patch(rect1)
+ ax.text(2.25, 8.8, "Ideał: 2 segmenty", fontsize=FS, ha="center", fontweight="bold")
+ ax.text(2.25, 7.5, "Segment A Segment B", fontsize=FS_SMALL, ha="center")
+ ax.text(
+ 2.25,
+ 6.7,
+ "(po marker-controlled)",
+ fontsize=FS_SMALL,
+ ha="center",
+ color=GREEN_ACCENT,
+ )
+
+ # Bad result (over-segmentation)
+ rect2 = FancyBboxPatch(
+ (5.5, 6),
+ 4,
+ 3.2,
+ boxstyle="round,pad=0.1",
+ facecolor="#FFCDD2",
+ edgecolor=BLACK,
+ linewidth=1,
+ )
+ ax.add_patch(rect2)
+ ax.text(
+ 7.5,
+ 8.8,
+ "Problem: over-segmentation",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ color=RED_ACCENT,
+ )
+ ax.text(
+ 7.5,
+ 7.8,
+ "47 regionów zamiast 2!",
+ fontsize=FS_SMALL,
+ ha="center",
+ color=RED_ACCENT,
+ )
+ ax.text(7.5, 7.1, "Każde mini-minimum", fontsize=FS_SMALL, ha="center")
+ ax.text(7.5, 6.5, '→ osobna „dolina"', fontsize=FS_SMALL, ha="center")
+
+ # Solution: markers
+ rect3 = FancyBboxPatch(
+ (1, 0.5),
+ 8,
+ 4.5,
+ boxstyle="round,pad=0.15",
+ facecolor=GRAY1,
+ edgecolor=GREEN_ACCENT,
+ linewidth=1.5,
+ )
+ ax.add_patch(rect3)
+ ax.text(
+ 5,
+ 4.3,
+ "Rozwiązanie: Marker-controlled watershed",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ color=GREEN_ACCENT,
+ )
+ ax.text(
+ 5,
+ 3.4,
+ '1. Zaznacz ręcznie „seeds" (markery) w każdym obiekcie',
+ fontsize=FS_SMALL,
+ ha="center",
+ )
+ ax.text(
+ 5,
+ 2.7,
+ "2. Zalewaj TYLKO od tych markerów (nie od wszystkich minimów)",
+ fontsize=FS_SMALL,
+ ha="center",
+ )
+ ax.text(
+ 5, 2.0, "3. Eliminuje fałszywe doliny z szumu", fontsize=FS_SMALL, ha="center"
+ )
+ ax.text(
+ 5,
+ 1.2,
+ "Wynik: tyle segmentów, ile podano markerów",
+ fontsize=FS_SMALL,
+ ha="center",
+ fontweight="bold",
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_watershed.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_watershed.png")
+
+
+# ============================================================
+# 3. MEAN SHIFT — Kernel, density, feature space
+# ============================================================
+def generate_mean_shift() -> None:
+ """Generate mean shift."""
+ _fig, axes = plt.subplots(1, 3, figsize=(11, 4))
+
+ # --- Panel 1: Feature space concept ---
+ ax = axes[0]
+ # Three clusters in 2D feature space (brightness, x-position)
+ c1x = rng.normal(2, 0.5, 40)
+ c1y = rng.normal(2, 0.5, 40)
+ c2x = rng.normal(6, 0.6, 35)
+ c2y = rng.normal(7, 0.5, 35)
+ c3x = rng.normal(8, 0.4, 25)
+ c3y = rng.normal(3, 0.6, 25)
+
+ ax.scatter(c1x, c1y, c=GRAY4, s=15, alpha=0.7, zorder=3)
+ ax.scatter(c2x, c2y, c=GRAY4, s=15, alpha=0.7, zorder=3)
+ ax.scatter(c3x, c3y, c=GRAY4, s=15, alpha=0.7, zorder=3)
+
+ # Label peaks
+ ax.scatter([2], [2], c=RED_ACCENT, s=80, marker="*", zorder=5, label="Max gęstości")
+ ax.scatter([6], [7], c=RED_ACCENT, s=80, marker="*", zorder=5)
+ ax.scatter([8], [3], c=RED_ACCENT, s=80, marker="*", zorder=5)
+
+ ax.set_xlabel("Cecha 1: jasność", fontsize=FS)
+ ax.set_ylabel("Cecha 2: pozycja x", fontsize=FS)
+ ax.set_title("Przestrzeń cech", fontsize=FS_TITLE, fontweight="bold")
+ ax.text(
+ 2, 0.3, "Klaster 1\n(ciemne, lewo)", ha="center", fontsize=FS_TINY, color=GRAY6
+ )
+ ax.text(
+ 6, 5.3, "Klaster 2\n(jasne, prawo)", ha="center", fontsize=FS_TINY, color=GRAY6
+ )
+ ax.text(
+ 8, 1.3, "Klaster 3\n(jasne, dół)", ha="center", fontsize=FS_TINY, color=GRAY6
+ )
+ ax.legend(fontsize=FS_SMALL, loc="upper left")
+
+ # --- Panel 2: Kernel/window moving ---
+ ax = axes[1]
+ ax.scatter(c1x, c1y, c=ACCENT_LIGHT, s=15, alpha=0.7, zorder=3)
+ ax.scatter(c2x, c2y, c=GRAY3, s=15, alpha=0.7, zorder=3)
+ ax.scatter(c3x, c3y, c=GRAY3, s=15, alpha=0.7, zorder=3)
+
+ # Show kernel movement
+ path_x = [4.5, 3.8, 3.0, 2.3, 2.05]
+ path_y = [4.0, 3.3, 2.7, 2.2, 2.03]
+
+ for i, (px, py) in enumerate(zip(path_x, path_y, strict=False)):
+ alpha = 0.3 + 0.15 * i
+ circle = plt.Circle(
+ (px, py),
+ 1.2,
+ fill=False,
+ edgecolor=ACCENT,
+ linewidth=1.5,
+ linestyle="--" if i < len(path_x) - 1 else "-",
+ alpha=alpha,
+ )
+ ax.add_patch(circle)
+ if i < len(path_x) - 1:
+ ax.annotate(
+ "",
+ xy=(path_x[i + 1], path_y[i + 1]),
+ xytext=(px, py),
+ arrowprops={"arrowstyle": "->", "color": RED_ACCENT, "lw": 1.5},
+ )
+
+ ax.scatter([path_x[0]], [path_y[0]], c=ACCENT, s=50, marker="o", zorder=5)
+ ax.scatter([path_x[-1]], [path_y[-1]], c=RED_ACCENT, s=80, marker="*", zorder=5)
+
+ ax.text(
+ 4.5, 5.2, "Start: losowy\npiksel", fontsize=FS_SMALL, ha="center", color=ACCENT
+ )
+ ax.text(
+ 2.05,
+ 0.5,
+ "Koniec: max\ngęstości",
+ fontsize=FS_SMALL,
+ ha="center",
+ color=RED_ACCENT,
+ fontweight="bold",
+ )
+ ax.text(
+ 7,
+ 8,
+ "Okno (jądro)\nprzesuwa się\ndo skupiska",
+ fontsize=FS_SMALL,
+ ha="center",
+ color=GRAY6,
+ bbox={"boxstyle": "round", "facecolor": GRAY1, "edgecolor": GRAY3},
+ )
+
+ ax.set_xlabel("Cecha 1", fontsize=FS)
+ ax.set_ylabel("Cecha 2", fontsize=FS)
+ ax.set_title("Jądro → max gęstości", fontsize=FS_TITLE, fontweight="bold")
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 9)
+
+ # --- Panel 3: Why no K parameter ---
+ ax = axes[2]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title("Dlaczego bez K?", fontsize=FS_TITLE, fontweight="bold")
+
+ y = 9.0
+ lines = [
+ ("K-means wymaga:", FS, RED_ACCENT, "bold"),
+ (' „Podaj K=3 klastry"', FS_SMALL, "black", "normal"),
+ (" Problem: skąd wiesz ile klastrów?", FS_SMALL, GRAY5, "normal"),
+ ("", 0, "", ""),
+ ("Mean Shift NIE wymaga K:", FS, GREEN_ACCENT, "bold"),
+ (" Każdy piksel startuje → toczy się", FS_SMALL, "black", "normal"),
+ (" → trafia do najbliższego szczytu", FS_SMALL, "black", "normal"),
+ (" → ile szczytów = tyle segmentów", FS_SMALL, "black", "normal"),
+ (" → automatycznie!", FS_SMALL, GREEN_ACCENT, "bold"),
+ ("", 0, "", ""),
+ ("Parametr: bandwidth (szerokość okna)", FS, "black", "bold"),
+ (" Duże okno → mało segmentów", FS_SMALL, "black", "normal"),
+ (" Małe okno → dużo segmentów", FS_SMALL, "black", "normal"),
+ ("", 0, "", ""),
+ ("Okno = jądro (kernel):", FS, "black", "bold"),
+ (" Koło o promieniu h wokół punktu.", FS_SMALL, "black", "normal"),
+ (" Oblicz średnią pikseli W oknie.", FS_SMALL, "black", "normal"),
+ (" Przesuń okno na tę średnią.", FS_SMALL, "black", "normal"),
+ (" Powtórz aż się zatrzyma.", FS_SMALL, "black", "normal"),
+ ]
+ for txt, size, color, weight in lines:
+ if txt == "":
+ y -= 0.2
+ continue
+ ax.text(0.5, y, txt, fontsize=size, color=color, fontweight=weight, va="top")
+ y -= 0.5
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_mean_shift.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_mean_shift.png")
+
+
+# ============================================================
+# 4. NORMALIZED CUTS — Graph cut visualization
+# ============================================================
+def generate_normalized_cuts() -> None:
+ """Generate normalized cuts."""
+ _fig, axes = plt.subplots(1, 3, figsize=(11, 4))
+
+ # --- Panel 1: Image as graph ---
+ ax = axes[0]
+ ax.set_xlim(-0.5, 4.5)
+ ax.set_ylim(-0.5, 4.5)
+ ax.set_aspect("equal")
+ ax.set_title("Obraz → graf", fontsize=FS_TITLE, fontweight="bold")
+
+ # Draw 4x4 pixel grid with colors
+ pixel_vals = np.array(
+ [
+ [30, 35, 180, 190],
+ [40, 30, 185, 200],
+ [170, 180, 40, 35],
+ [190, 175, 30, 45],
+ ]
+ )
+ for i in range(4):
+ for j in range(4):
+ v = pixel_vals[i, j]
+ gray_val = v / 255.0
+ str(gray_val)
+ rect = patches.Rectangle(
+ (j - 0.4, 3 - i - 0.4),
+ 0.8,
+ 0.8,
+ facecolor=(gray_val, gray_val, gray_val),
+ edgecolor=BLACK,
+ linewidth=0.8,
+ )
+ ax.add_patch(rect)
+ text_color = "white" if v < 100 else "black"
+ ax.text(
+ j,
+ 3 - i,
+ str(v),
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ color=text_color,
+ fontweight="bold",
+ )
+
+ # Draw edges between adjacent pixels
+ for i in range(4):
+ for j in range(4):
+ # Right neighbor
+ if j < 3:
+ similarity = max(
+ 0, 1 - abs(pixel_vals[i, j] - pixel_vals[i, j + 1]) / 255
+ )
+ lw = similarity * 2.5 + 0.3
+ alpha = similarity * 0.8 + 0.2
+ ax.plot(
+ [j + 0.4, j + 0.6],
+ [3 - i, 3 - i],
+ color=GRAY5,
+ linewidth=lw,
+ alpha=alpha,
+ )
+ # Bottom neighbor
+ if i < 3:
+ similarity = max(
+ 0, 1 - abs(pixel_vals[i, j] - pixel_vals[i + 1, j]) / 255
+ )
+ lw = similarity * 2.5 + 0.3
+ alpha = similarity * 0.8 + 0.2
+ ax.plot(
+ [j, j],
+ [3 - i - 0.4, 3 - i - 0.6],
+ color=GRAY5,
+ linewidth=lw,
+ alpha=alpha,
+ )
+
+ ax.text(
+ 2,
+ -0.8,
+ "Grube linie = duże podobieństwo\n(silna krawędź grafu)",
+ ha="center",
+ fontsize=FS_TINY,
+ color=GRAY5,
+ )
+ ax.axis("off")
+
+ # --- Panel 2: Cut concept ---
+ ax = axes[1]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title("Cięcie grafu (graph cut)", fontsize=FS_TITLE, fontweight="bold")
+
+ # Draw two groups of nodes
+ # Group A (dark pixels)
+ positions_A = [(2, 7), (3, 8), (2, 5), (3, 6)]
+ positions_B = [(7, 7), (8, 8), (7, 5), (8, 6)]
+
+ # Intra-group edges (thick = similar)
+ for i, (x1, y1) in enumerate(positions_A):
+ for x2, y2 in positions_A[i + 1 :]:
+ ax.plot([x1, x2], [y1, y2], color=ACCENT, linewidth=2, alpha=0.5)
+ for i, (x1, y1) in enumerate(positions_B):
+ for x2, y2 in positions_B[i + 1 :]:
+ ax.plot([x1, x2], [y1, y2], color=RED_ACCENT, linewidth=2, alpha=0.5)
+
+ # Inter-group edges (thin = dissimilar) — these get cut
+ cut_edges = [((3, 8), (7, 7)), ((3, 6), (7, 5)), ((2, 5), (7, 5))]
+ for (x1, y1), (x2, y2) in cut_edges:
+ ax.plot([x1, x2], [y1, y2], color=GRAY4, linewidth=0.8, linestyle="--")
+
+ # Draw nodes
+ for x, y in positions_A:
+ ax.scatter(x, y, c=ACCENT, s=120, zorder=5, edgecolors=BLACK, linewidth=0.8)
+ for x, y in positions_B:
+ ax.scatter(x, y, c="#FFCDD2", s=120, zorder=5, edgecolors=BLACK, linewidth=0.8)
+
+ # Cut line
+ ax.plot(
+ [5, 5], [3.5, 9.5], color=RED_ACCENT, linewidth=2.5, linestyle="-", zorder=4
+ )
+ ax.text(
+ 5, 9.8, "CIĘCIE", ha="center", fontsize=FS, fontweight="bold", color=RED_ACCENT
+ )
+
+ ax.text(
+ 2.5,
+ 3.8,
+ "Segment A\n(ciemne piksele)",
+ ha="center",
+ fontsize=FS_SMALL,
+ color=ACCENT,
+ )
+ ax.text(
+ 7.5,
+ 3.8,
+ "Segment B\n(jasne piksele)",
+ ha="center",
+ fontsize=FS_SMALL,
+ color=RED_ACCENT,
+ )
+
+ # Formula
+ ax.text(
+ 5,
+ 1.8,
+ "Ncut(A,B) = cut(A,B)/assoc(A,V)\n + cut(A,B)/assoc(B,V)",
+ ha="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ bbox={"boxstyle": "round", "facecolor": GRAY1, "edgecolor": GRAY3},
+ )
+ ax.text(
+ 5,
+ 0.5,
+ "Minimalizuj Ncut → tnij SŁABE krawędzie\nzachowuj SILNE (wewnątrz grupy)",
+ ha="center",
+ fontsize=FS_TINY,
+ color=GRAY5,
+ )
+
+ # --- Panel 3: Algorithm summary ---
+ ax = axes[2]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title("Algorytm Normalized Cuts", fontsize=FS_TITLE, fontweight="bold")
+
+ steps = [
+ (
+ "1. Zbuduj graf",
+ "Piksele = węzły\nKrawędzie = podobieństwo sąsiadów\n(kolor, jasność, odległość)",
+ ),
+ (
+ "2. Macierz podobieństwa W",
+ "W[i,j] = exp(-|kolori - kolorj|² / σ²)\n→ im podobniejsze, tym wyższa waga",
+ ),
+ ("3. Macierz stopni D", "D[i,i] = Σ W[i,j]\n(suma wszystkich wag z węzła i)"),
+ ("4. Rozwiąż problem własny", "(D-W)·y = λ·D·y\n→ drugi najm. wektor własny y"),
+ ("5. Podziel wg y", "y[i] > 0 → segment A\ny[i] ≤ 0 → segment B"),
+ ]
+
+ y = 9.5
+ for title, desc in steps:
+ ax.text(0.5, y, title, fontsize=FS, fontweight="bold", va="top")
+ y -= 0.4
+ ax.text(0.8, y, desc, fontsize=FS_TINY, va="top", color=GRAY6)
+ y -= 1.2
+
+ ax.text(
+ 5,
+ 0.3,
+ "Złożoność: O(n³) — wymaga eigen decomposition!",
+ ha="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ color=RED_ACCENT,
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_normalized_cuts.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_normalized_cuts.png")
+
+
+# ============================================================
+# 5. RELU — Function plot
+# ============================================================
+def generate_relu() -> None:
+ """Generate relu."""
+ _fig, axes = plt.subplots(1, 2, figsize=(8, 3.5))
+
+ # --- Panel 1: ReLU plot ---
+ ax = axes[0]
+ x = np.linspace(-5, 5, 200)
+ relu = np.maximum(0, x)
+ ax.plot(x, relu, color=ACCENT, linewidth=2.5, label="ReLU(x) = max(0, x)")
+ ax.axhline(y=0, color=GRAY3, linewidth=0.5)
+ ax.axvline(x=0, color=GRAY3, linewidth=0.5)
+ ax.fill_between(x[x < 0], 0, 0, color=RED_ACCENT, alpha=0.1)
+ ax.fill_between(x[x >= 0], 0, relu[x >= 0], color=ACCENT, alpha=0.1)
+
+ # Annotations
+ ax.annotate(
+ 'x < 0 → output = 0\n(neuron „wyłączony")',
+ xy=(-3, 0),
+ fontsize=FS_SMALL,
+ ha="center",
+ va="bottom",
+ color=RED_ACCENT,
+ arrowprops={"arrowstyle": "->", "color": RED_ACCENT},
+ xytext=(-3, 2),
+ )
+ ax.annotate(
+ 'x ≥ 0 → output = x\n(neuron „włączony")',
+ xy=(3, 3),
+ fontsize=FS_SMALL,
+ ha="center",
+ va="bottom",
+ color=ACCENT,
+ arrowprops={"arrowstyle": "->", "color": ACCENT},
+ xytext=(3, 4.5),
+ )
+ ax.scatter([0], [0], c=BLACK, s=40, zorder=5)
+ ax.text(0.3, -0.5, "(0,0)", fontsize=FS_SMALL, color=GRAY5)
+ ax.set_xlabel("x (wejście neuronu)", fontsize=FS)
+ ax.set_ylabel("ReLU(x)", fontsize=FS)
+ ax.set_title("ReLU — Rectified Linear Unit", fontsize=FS_TITLE, fontweight="bold")
+ ax.legend(fontsize=FS_SMALL, loc="upper left")
+ ax.set_ylim(-1, 6)
+ ax.grid(True, alpha=0.2)
+
+ # --- Panel 2: Why ReLU ---
+ ax = axes[1]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title("Dlaczego ReLU?", fontsize=FS_TITLE, fontweight="bold")
+
+ y = 9.0
+ lines = [
+ ("Neuron oblicza:", FS, BLACK, "bold"),
+ (" z = w₁·x₁ + w₂·x₂ + ... + bias", FS_SMALL, BLACK, "normal"),
+ (" output = ReLU(z) = max(0, z)", FS_SMALL, ACCENT, "bold"),
+ ("", 0, "", ""),
+ ("Przykład:", FS, BLACK, "bold"),
+ (" wagi: w₁=0.5, w₂=-0.3, bias=0.1", FS_SMALL, BLACK, "normal"),
+ (" wejścia: x₁=2.0, x₂=4.0", FS_SMALL, BLACK, "normal"),
+ (" z = 0.5·2 + (-0.3)·4 + 0.1 = -0.1", FS_SMALL, BLACK, "normal"),
+ (" ReLU(-0.1) = max(0, -0.1) = 0", FS_SMALL, RED_ACCENT, "bold"),
+ (" → neuron milczy (wejście nieistotne)", FS_SMALL, GRAY5, "normal"),
+ ("", 0, "", ""),
+ ("Gdyby z = 2.3:", FS, BLACK, "bold"),
+ (" ReLU(2.3) = max(0, 2.3) = 2.3", FS_SMALL, GREEN_ACCENT, "bold"),
+ (" → neuron aktywny! Przekazuje sygnał", FS_SMALL, GRAY5, "normal"),
+ ("", 0, "", ""),
+ ("Szybsza niż sigmoid/tanh", FS_SMALL, GRAY5, "normal"),
+ ("(brak exp() → szybkie obliczenia)", FS_SMALL, GRAY5, "normal"),
+ ]
+ for txt, size, color, weight in lines:
+ if txt == "":
+ y -= 0.2
+ continue
+ ax.text(0.5, y, txt, fontsize=size, color=color, fontweight=weight, va="top")
+ y -= 0.5
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_relu.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_relu.png")
+
+
+# ============================================================
+# 6. DOT PRODUCT — Iloczyn skalarny visual
+# ============================================================
+def generate_dot_product() -> None:
+ """Generate dot product."""
+ _fig, axes = plt.subplots(1, 3, figsize=(11, 3.5))
+
+ # --- Panel 1: Concept ---
+ ax = axes[0]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title(
+ "Iloczyn skalarny\n(dot product)", fontsize=FS_TITLE, fontweight="bold"
+ )
+
+ y = 8.5
+ lines = [
+ ("Dwa wektory (listy liczb) → JEDNA liczba", FS, BLACK, "bold"),
+ ("", 0, "", ""),
+ ("a = [a₁, a₂, a₃] b = [b₁, b₂, b₃]", FS, ACCENT, "normal"),
+ ("", 0, "", ""),
+ ("a · b = a₁·b₁ + a₂·b₂ + a₃·b₃", FS, BLACK, "bold"),
+ ("", 0, "", ""),
+ ("Przykład:", FS, BLACK, "bold"),
+ ("a = [1, 3, -2] b = [4, -1, 5]", FS_SMALL, BLACK, "normal"),
+ ("a·b = 1·4 + 3·(-1) + (-2)·5", FS_SMALL, BLACK, "normal"),
+ (" = 4 + (-3) + (-10) = -9", FS_SMALL, RED_ACCENT, "bold"),
+ ("", 0, "", ""),
+ (
+ 'Duży wynik → wektory „podobne" (w tym samym kierunku)',
+ FS_SMALL,
+ GREEN_ACCENT,
+ "normal",
+ ),
+ ('Mały/ujemny → wektory „różne"', FS_SMALL, RED_ACCENT, "normal"),
+ ]
+ for txt, size, color, weight in lines:
+ if txt == "":
+ y -= 0.25
+ continue
+ ax.text(0.5, y, txt, fontsize=size, color=color, fontweight=weight, va="top")
+ y -= 0.55
+
+ # --- Panel 2: Convolution as dot product ---
+ ax = axes[1]
+ ax.set_xlim(-0.5, 5.5)
+ ax.set_ylim(-0.5, 5.5)
+ ax.set_aspect("equal")
+ ax.set_title(
+ "Konwolucja = iloczyn skalarny\nfiltra x fragment obrazu",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ )
+
+ # Filter 3x3
+ filter_vals = [[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]
+ for i in range(3):
+ for j in range(3):
+ rect = patches.Rectangle(
+ (j - 0.4, 4 - i - 0.4),
+ 0.8,
+ 0.8,
+ facecolor=ACCENT_LIGHT,
+ edgecolor=BLACK,
+ linewidth=0.8,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ j,
+ 4 - i,
+ str(filter_vals[i][j]),
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ ax.text(1, 1.5, "Filtr", ha="center", fontsize=FS, fontweight="bold", color=ACCENT)
+
+ # Image patch
+ img_vals = [[50, 50, 200], [50, 50, 200], [50, 50, 200]]
+ for i in range(3):
+ for j in range(3):
+ rect = patches.Rectangle(
+ (j + 2.6, 4 - i - 0.4),
+ 0.8,
+ 0.8,
+ facecolor=GRAY2,
+ edgecolor=BLACK,
+ linewidth=0.8,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ j + 3,
+ 4 - i,
+ str(img_vals[i][j]),
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ ax.text(
+ 4,
+ 1.5,
+ "Fragment\nobrazu",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ color=GRAY5,
+ )
+
+ ax.text(
+ 2.5,
+ 0.5,
+ "(-1)·50 + 0·50 + 1·200 +\n(-1)·50 + 0·50 + 1·200 +\n(-1)·50 + 0·50 + 1·200\n= 450 (krawędź!)",
+ ha="center",
+ fontsize=FS_TINY,
+ fontweight="bold",
+ bbox={"boxstyle": "round", "facecolor": GRAY1, "edgecolor": GREEN_ACCENT},
+ )
+
+ ax.axis("off")
+
+ # --- Panel 3: Vector visualization ---
+ ax = axes[2]
+ # Draw two vectors
+ ax.quiver(
+ 0,
+ 0,
+ 3,
+ 4,
+ angles="xy",
+ scale_units="xy",
+ scale=1,
+ color=ACCENT,
+ width=0.025,
+ label="a = [3, 4]",
+ )
+ ax.quiver(
+ 0,
+ 0,
+ 4,
+ 1,
+ angles="xy",
+ scale_units="xy",
+ scale=1,
+ color=RED_ACCENT,
+ width=0.025,
+ label="b = [4, 1]",
+ )
+
+ # Show angle
+ theta = np.linspace(np.arctan2(1, 4), np.arctan2(4, 3), 30)
+ r = 1.5
+ ax.plot(r * np.cos(theta), r * np.sin(theta), color=GREEN_ACCENT, linewidth=1.5)
+ ax.text(1.8, 1.3, "θ", fontsize=FS, color=GREEN_ACCENT, fontweight="bold")
+
+ ax.text(3.2, 4.2, "a", fontsize=FS, color=ACCENT, fontweight="bold")
+ ax.text(4.2, 1.2, "b", fontsize=FS, color=RED_ACCENT, fontweight="bold")
+
+ ax.text(
+ 2.5,
+ -1.0,
+ "a · b = |a|·|b|·cos(θ)\n= 3·4 + 4·1 = 16",
+ ha="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ bbox={"boxstyle": "round", "facecolor": GRAY1, "edgecolor": GRAY3},
+ )
+ ax.text(
+ 2.5,
+ -2.0,
+ 'Mały kąt θ → duży dot product\n= wektory „zgadają się"',
+ ha="center",
+ fontsize=FS_TINY,
+ color=GRAY5,
+ )
+
+ ax.set_xlim(-0.5, 5.5)
+ ax.set_ylim(-2.5, 5.5)
+ ax.set_aspect("equal")
+ ax.grid(True, alpha=0.2)
+ ax.legend(fontsize=FS_SMALL, loc="upper left")
+ ax.set_title("Geometrycznie: kąt", fontsize=FS_TITLE, fontweight="bold")
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_dot_product.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_dot_product.png")
+
+
+# ============================================================
+# 7. FCN — FC vs Conv 1x1, skip connections
+# ============================================================
+def generate_fcn() -> None:
+ """Generate fcn."""
+ _fig, axes = plt.subplots(2, 1, figsize=(10, 7))
+
+ # --- Panel 1: FC vs Conv 1x1 ---
+ ax = axes[0]
+ ax.set_xlim(0, 20)
+ ax.set_ylim(0, 6)
+ ax.axis("off")
+ ax.set_title(
+ "FC (Fully Connected) vs Conv 1x1", fontsize=FS_TITLE, fontweight="bold"
+ )
+
+ # Classic CNN with FC
+ layer_info_fc = [
+ (1.5, "Obraz\n224x224x3", 2.2, GRAY2),
+ (4.5, "Conv+Pool\n112x112x64", 1.8, GRAY2),
+ (7.5, "Conv+Pool\n7x7x512", 1.0, GRAY2),
+ (10, "Flatten\n25088", 0.5, ACCENT_LIGHT),
+ (12, "FC\n4096", 0.5, ACCENT_LIGHT),
+ (14, "FC\n1000", 0.3, ACCENT_LIGHT),
+ (16, '"Kot"', 0.3, "#FFCDD2"),
+ ]
+
+ y_fc = 4.5
+ for i, (x, label, w, color) in enumerate(layer_info_fc):
+ rect = FancyBboxPatch(
+ (x - w / 2, y_fc - 0.6),
+ w,
+ 1.2,
+ boxstyle="round,pad=0.05",
+ facecolor=color,
+ edgecolor=BLACK,
+ linewidth=0.8,
+ )
+ ax.add_patch(rect)
+ ax.text(x, y_fc, label, ha="center", va="center", fontsize=FS_TINY)
+ if i < len(layer_info_fc) - 1:
+ next_x = layer_info_fc[i + 1][0]
+ ax.annotate(
+ "",
+ xy=(next_x - layer_info_fc[i + 1][2] / 2, y_fc),
+ xytext=(x + w / 2, y_fc),
+ arrowprops={"arrowstyle": "->", "color": GRAY5, "lw": 1},
+ )
+
+ ax.text(
+ 0.3, y_fc, "CNN:", fontsize=FS, fontweight="bold", color=RED_ACCENT, va="center"
+ )
+ ax.text(
+ 12,
+ y_fc + 1,
+ "PROBLEM: FC wymaga\nSTAŁEGO rozmiaru\n(np. 224x224)",
+ ha="center",
+ fontsize=FS_SMALL,
+ color=RED_ACCENT,
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round",
+ "facecolor": "#FFCDD2",
+ "edgecolor": RED_ACCENT,
+ "alpha": 0.3,
+ },
+ )
+
+ # FCN with Conv 1x1
+ layer_info_fcn = [
+ (1.5, "Obraz\nHxWx3", 2.2, GRAY2),
+ (4.5, "Conv+Pool\nH/2 x W/2\nx64", 1.8, GRAY2),
+ (7.5, "Conv+Pool\nH/32 x W/32\nx512", 1.0, GRAY2),
+ (10.5, "Conv 1x1\nH/32 x W/32\nxC", 0.8, "#C8E6C9"),
+ (13.5, "Upsample\nHxWxC", 1.8, "#C8E6C9"),
+ (16.5, "Mapa\nsegmentacji", 1.5, "#C8E6C9"),
+ ]
+
+ y_fcn = 1.5
+ for i, (x, label, w, color) in enumerate(layer_info_fcn):
+ rect = FancyBboxPatch(
+ (x - w / 2, y_fcn - 0.7),
+ w,
+ 1.4,
+ boxstyle="round,pad=0.05",
+ facecolor=color,
+ edgecolor=BLACK,
+ linewidth=0.8,
+ )
+ ax.add_patch(rect)
+ ax.text(x, y_fcn, label, ha="center", va="center", fontsize=FS_TINY)
+ if i < len(layer_info_fcn) - 1:
+ next_x = layer_info_fcn[i + 1][0]
+ ax.annotate(
+ "",
+ xy=(next_x - layer_info_fcn[i + 1][2] / 2, y_fcn),
+ xytext=(x + w / 2, y_fcn),
+ arrowprops={"arrowstyle": "->", "color": GRAY5, "lw": 1},
+ )
+
+ ax.text(
+ 0.3,
+ y_fcn,
+ "FCN:",
+ fontsize=FS,
+ fontweight="bold",
+ color=GREEN_ACCENT,
+ va="center",
+ )
+ ax.text(
+ 10.5,
+ y_fcn + 1.2,
+ "Conv 1x1:\nkażdy piksel\nosobno x wagi\n(jak FC ale\nzachowuje HxW)",
+ ha="center",
+ fontsize=FS_TINY,
+ color=GREEN_ACCENT,
+ bbox={
+ "boxstyle": "round",
+ "facecolor": "#C8E6C9",
+ "edgecolor": GREEN_ACCENT,
+ "alpha": 0.3,
+ },
+ )
+
+ # --- Panel 2: What FC and Conv do ---
+ ax = axes[1]
+ ax.set_xlim(0, 20)
+ ax.set_ylim(0, 6)
+ ax.axis("off")
+ ax.set_title(
+ "Co robi warstwa FC? Co robi konwolucja?", fontsize=FS_TITLE, fontweight="bold"
+ )
+
+ # FC explanation
+ rect = FancyBboxPatch(
+ (0.3, 3.2),
+ 9,
+ 2.5,
+ boxstyle="round,pad=0.15",
+ facecolor=ACCENT_LIGHT,
+ edgecolor=ACCENT,
+ linewidth=1,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 4.8, 5.2, "Fully Connected (FC)", fontsize=FS, fontweight="bold", ha="center"
+ )
+ ax.text(
+ 4.8,
+ 4.5,
+ "KAŻDY neuron połączony z KAŻDYM wejściem\n"
+ "25 088 wejść x 4 096 neuronów = ~103 MLN wag!\n"
+ "Traci informację GDZIE (przestrzenną)\n"
+ "Wymaga STAŁEGO rozmiaru wejścia",
+ fontsize=FS_TINY,
+ ha="center",
+ va="top",
+ )
+
+ # Conv explanation
+ rect = FancyBboxPatch(
+ (10.3, 3.2),
+ 9,
+ 2.5,
+ boxstyle="round,pad=0.15",
+ facecolor="#C8E6C9",
+ edgecolor=GREEN_ACCENT,
+ linewidth=1,
+ )
+ ax.add_patch(rect)
+ ax.text(14.8, 5.2, "Konwolucja (Conv)", fontsize=FS, fontweight="bold", ha="center")
+ ax.text(
+ 14.8,
+ 4.5,
+ 'Filtr (np. 3x3) „jedzie" po obrazie\n'
+ "Te same wagi dla KAŻDEJ pozycji\n"
+ "Zachowuje informację GDZIE\n"
+ "Akceptuje DOWOLNY rozmiar wejścia",
+ fontsize=FS_TINY,
+ ha="center",
+ va="top",
+ )
+
+ # Conv 1x1 explanation
+ rect = FancyBboxPatch(
+ (3, 0.3),
+ 14,
+ 2.2,
+ boxstyle="round,pad=0.15",
+ facecolor=GRAY1,
+ edgecolor=BLACK,
+ linewidth=1,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 10,
+ 2.1,
+ 'Conv 1x1 = „FC per piksel"',
+ fontsize=FS,
+ fontweight="bold",
+ ha="center",
+ )
+ ax.text(
+ 10,
+ 1.5,
+ "Filtr 1x1: patrzy na JEDEN piksel, ale WSZYSTKIE kanały (512→C klas)\n"
+ "Działa jak FC ale zachowuje mapę HxW → każdy piksel osobno klasyfikowany\n"
+ "FCN: zamień FC na Conv1x1 → koniec z wymogiem stałego rozmiaru!",
+ fontsize=FS_TINY,
+ ha="center",
+ va="top",
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_fc_vs_conv1x1.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_fc_vs_conv1x1.png")
+
+
+# ============================================================
+# 8. U-NET ARCHITECTURE — Proper U-shaped diagram
+# ============================================================
+def generate_unet() -> None:
+ """Generate unet."""
+ _fig, ax = plt.subplots(1, 1, figsize=(10, 6))
+ ax.set_xlim(-1, 21)
+ ax.set_ylim(-1, 12)
+ ax.axis("off")
+ ax.set_title(
+ "U-Net: architektura w kształcie litery U",
+ fontsize=FS_TITLE + 1,
+ fontweight="bold",
+ )
+
+ # Encoder layers (going DOWN-LEFT)
+ encoder_layers = [
+ (2, 10, 2.5, 1.5, "572x572x1\n(wejście)", 64),
+ (2, 7.5, 2.2, 1.3, "284x284\nx64", 64),
+ (2, 5, 1.8, 1.1, "140x140\nx128", 128),
+ (2, 2.5, 1.5, 1.0, "68x68\nx256", 256),
+ ]
+
+ # Bottleneck
+ bottleneck = (8, 0.5, 2.5, 1.2, "32x32x512\n(bottleneck)", 512)
+
+ # Decoder layers (going UP-RIGHT)
+ decoder_layers = [
+ (14, 2.5, 1.5, 1.0, "68x68\nx256", 256),
+ (14, 5, 1.8, 1.1, "140x140\nx128", 128),
+ (14, 7.5, 2.2, 1.3, "284x284\nx64", 64),
+ (14, 10, 2.5, 1.5, "572x572xC\n(mapa seg.)", "C"),
+ ]
+
+ def draw_block(ax, x, y, w, h, label, color) -> None:
+ """Draw block."""
+ rect = FancyBboxPatch(
+ (x - w / 2, y - h / 2),
+ w,
+ h,
+ boxstyle="round,pad=0.05",
+ facecolor=color,
+ edgecolor=BLACK,
+ linewidth=1.2,
+ )
+ ax.add_patch(rect)
+ ax.text(x, y, label, ha="center", va="center", fontsize=FS_TINY)
+
+ # Draw encoder
+ for x, y, w, h, label, _channels in encoder_layers:
+ draw_block(ax, x, y, w, h, label, ACCENT_LIGHT)
+
+ # Draw arrows down (encoder)
+ for i in range(len(encoder_layers) - 1):
+ x1, y1 = encoder_layers[i][0], encoder_layers[i][1] - encoder_layers[i][3] / 2
+ x2, y2 = (
+ encoder_layers[i + 1][0],
+ encoder_layers[i + 1][1] + encoder_layers[i + 1][3] / 2,
+ )
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": "->", "color": ACCENT, "lw": 2},
+ )
+ ax.text(
+ x1 - 1.7,
+ (y1 + y2) / 2,
+ "MaxPool\n2x2\n↓ zmniejsz",
+ fontsize=FS_TINY,
+ ha="center",
+ color=ACCENT,
+ fontweight="bold",
+ )
+
+ # Encoder to bottleneck
+ x1, y1 = encoder_layers[-1][0], encoder_layers[-1][1] - encoder_layers[-1][3] / 2
+ draw_block(
+ ax,
+ bottleneck[0],
+ bottleneck[1],
+ bottleneck[2],
+ bottleneck[3],
+ bottleneck[4],
+ GRAY2,
+ )
+ ax.annotate(
+ "",
+ xy=(bottleneck[0] - bottleneck[2] / 2, bottleneck[1] + bottleneck[3] / 2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": "->", "color": ACCENT, "lw": 2},
+ )
+
+ # Bottleneck to decoder
+ ax.annotate(
+ "",
+ xy=(
+ decoder_layers[0][0] - decoder_layers[0][2] / 2,
+ decoder_layers[0][1] - decoder_layers[0][3] / 2,
+ ),
+ xytext=(bottleneck[0] + bottleneck[2] / 2, bottleneck[1] + bottleneck[3] / 2),
+ arrowprops={"arrowstyle": "->", "color": RED_ACCENT, "lw": 2},
+ )
+
+ # Draw decoder
+ for x, y, w, h, label, channels in decoder_layers:
+ color = "#C8E6C9" if channels != "C" else "#A5D6A7"
+ draw_block(ax, x, y, w, h, label, color)
+
+ # Draw arrows up (decoder)
+ for i in range(len(decoder_layers) - 1):
+ x1, y1 = decoder_layers[i][0], decoder_layers[i][1] + decoder_layers[i][3] / 2
+ x2, y2 = (
+ decoder_layers[i + 1][0],
+ decoder_layers[i + 1][1] - decoder_layers[i + 1][3] / 2,
+ )
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": "->", "color": GREEN_ACCENT, "lw": 2},
+ )
+ ax.text(
+ x1 + 2,
+ (y1 + y2) / 2,
+ "UpConv\n2x2\n↑ zwiększ",
+ fontsize=FS_TINY,
+ ha="center",
+ color=GREEN_ACCENT,
+ fontweight="bold",
+ )
+
+ # Skip connections (horizontal arrows)
+ for i in range(len(encoder_layers)):
+ enc = encoder_layers[i]
+ dec = decoder_layers[len(decoder_layers) - 1 - i]
+ ax.annotate(
+ "",
+ xy=(dec[0] - dec[2] / 2, dec[1]),
+ xytext=(enc[0] + enc[2] / 2, enc[1]),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": GRAY5,
+ "lw": 1.5,
+ "linestyle": "dashed",
+ },
+ )
+ mid_x = (enc[0] + enc[2] / 2 + dec[0] - dec[2] / 2) / 2
+ ax.text(
+ mid_x,
+ enc[1] + 0.6,
+ "skip\n(concat)",
+ fontsize=FS_TINY,
+ ha="center",
+ color=GRAY5,
+ fontweight="bold",
+ )
+
+ # Labels
+ ax.text(
+ 0,
+ 11.5,
+ "ENCODER\n(↓ zmniejsza)",
+ fontsize=FS,
+ fontweight="bold",
+ color=ACCENT,
+ ha="center",
+ )
+ ax.text(
+ 17,
+ 11.5,
+ "DECODER\n(↑ zwiększa)",
+ fontsize=FS,
+ fontweight="bold",
+ color=GREEN_ACCENT,
+ ha="center",
+ )
+ ax.text(
+ 8,
+ -0.8,
+ 'Kształt litery „U": encoder schodzi ↓ → bottleneck na dnie → decoder wraca ↑',
+ fontsize=FS_SMALL,
+ ha="center",
+ color=GRAY5,
+ fontweight="bold",
+ )
+
+ # Concatenation explanation
+ rect = FancyBboxPatch(
+ (17.5, 3),
+ 3,
+ 5,
+ boxstyle="round,pad=0.15",
+ facecolor=GRAY1,
+ edgecolor=GRAY5,
+ linewidth=1,
+ linestyle="--",
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 19, 7.5, "Concatenation:", fontsize=FS_SMALL, ha="center", fontweight="bold"
+ )
+ ax.text(
+ 19,
+ 6.5,
+ "Encoder: 64 kanały\nDecoder: 64 kanały\n→ concat → 128 kanałów\n\n"
+ "Jak sklejenie\ndwóch stosów\nkart:",
+ fontsize=FS_TINY,
+ ha="center",
+ )
+ ax.text(
+ 19,
+ 3.7,
+ "[enc₁|enc₂|...|dec₁|dec₂|...]",
+ fontsize=FS_TINY - 1,
+ ha="center",
+ fontweight="bold",
+ color=ACCENT,
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_unet_arch.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_unet_arch.png")
+
+
+# ============================================================
+# 9. RECEPTIVE FIELD — with dilation
+# ============================================================
+def generate_receptive_field() -> None:
+ """Generate receptive field."""
+ _fig, axes = plt.subplots(1, 3, figsize=(11, 4))
+
+ def draw_grid(
+ ax, size, highlight_cells, highlight_color, title, grid_offset=(0, 0)
+ ) -> None:
+ """Draw grid."""
+ ox, oy = grid_offset
+ for i in range(size):
+ for j in range(size):
+ color = WHITE
+ if (i, j) in highlight_cells:
+ color = highlight_color
+ rect = patches.Rectangle(
+ (ox + j, oy + size - 1 - i),
+ 1,
+ 1,
+ facecolor=color,
+ edgecolor=GRAY4,
+ linewidth=0.5,
+ )
+ ax.add_patch(rect)
+ ax.set_title(title, fontsize=FS_TITLE, fontweight="bold")
+
+ # --- Panel 1: Standard 3x3 conv receptive field ---
+ ax = axes[0]
+ ax.set_xlim(-0.5, 7.5)
+ ax.set_ylim(-1, 8)
+ ax.set_aspect("equal")
+ ax.axis("off")
+
+ # 7x7 input grid
+ highlight_3x3 = [
+ (2, 2),
+ (2, 3),
+ (2, 4),
+ (3, 2),
+ (3, 3),
+ (3, 4),
+ (4, 2),
+ (4, 3),
+ (4, 4),
+ ]
+ draw_grid(ax, 7, highlight_3x3, ACCENT_LIGHT, "Zwykła conv 3x3")
+ ax.text(
+ 3.5,
+ -0.5,
+ "RF = 3x3 pikseli",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ color=ACCENT,
+ )
+
+ # --- Panel 2: Dilated conv (rate=2) ---
+ ax = axes[1]
+ ax.set_xlim(-0.5, 7.5)
+ ax.set_ylim(-1, 8)
+ ax.set_aspect("equal")
+ ax.axis("off")
+
+ # 7x7 input grid with dilated highlights
+ highlight_dilated = [
+ (1, 1),
+ (1, 3),
+ (1, 5),
+ (3, 1),
+ (3, 3),
+ (3, 5),
+ (5, 1),
+ (5, 3),
+ (5, 5),
+ ]
+ draw_grid(ax, 7, highlight_dilated, "#FFCDD2", "Dilated conv 3x3\n(rate=2)")
+ ax.text(
+ 3.5,
+ -0.5,
+ "RF = 5x5, ale 9 parametrów!",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ color=RED_ACCENT,
+ )
+
+ # Connect dots to show pattern
+ dots_x = [1.5, 3.5, 5.5, 1.5, 3.5, 5.5, 1.5, 3.5, 5.5]
+ dots_y = [5.5, 5.5, 5.5, 3.5, 3.5, 3.5, 1.5, 1.5, 1.5]
+ ax.scatter(dots_x, dots_y, c=RED_ACCENT, s=30, zorder=5)
+
+ # --- Panel 3: Comparison ---
+ ax = axes[2]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title(
+ "Receptive Field\n(pole widzenia neuronu)", fontsize=FS_TITLE, fontweight="bold"
+ )
+
+ y = 8.5
+ lines = [
+ ("RF = ile pikseli WEJŚCIOWYCH", FS, BLACK, "bold"),
+ ("wpływa na JEDEN piksel wyjścia", FS, BLACK, "bold"),
+ ("", 0, "", ""),
+ ("Rate (współczynnik dylatacji):", FS, BLACK, "bold"),
+ (' rate=1: filtr „dotyka" sąsiadów', FS_SMALL, BLACK, "normal"),
+ (" rate=2: co drugi piksel → RF = 5x5", FS_SMALL, BLACK, "normal"),
+ (" rate=3: co trzeci → RF = 7x7", FS_SMALL, BLACK, "normal"),
+ (" WIĘCEJ kontekstu, TE SAME wagi!", FS_SMALL, GREEN_ACCENT, "bold"),
+ ("", 0, "", ""),
+ ("Dlaczego ważne w segmentacji?", FS, BLACK, "bold"),
+ (" Piksel sam nie wie czym jest.", FS_SMALL, BLACK, "normal"),
+ (" Potrzebuje KONTEKSTU (otoczenia).", FS_SMALL, BLACK, "normal"),
+ (" Większe RF → widzi obok budynki", FS_SMALL, BLACK, "normal"),
+ (' → wie, że TEN piksel to „droga"', FS_SMALL, GREEN_ACCENT, "bold"),
+ ("", 0, "", ""),
+ ("Global Average Pooling:", FS, BLACK, "bold"),
+ (" Mapa HxWxC → 1x1xC", FS_SMALL, BLACK, "normal"),
+ (" Średnia z CAŁEGO feature map", FS_SMALL, BLACK, "normal"),
+ (" RF = nieskończone (cały obraz)", FS_SMALL, GREEN_ACCENT, "bold"),
+ ]
+ for txt, size, color, weight in lines:
+ if txt == "":
+ y -= 0.2
+ continue
+ ax.text(0.5, y, txt, fontsize=size, color=color, fontweight=weight, va="top")
+ y -= 0.45
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_receptive_field.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_receptive_field.png")
+
+
+# ============================================================
+# 10. TRANSFORMER / Self-attention / SOTA
+# ============================================================
+def generate_transformer() -> None:
+ """Generate transformer."""
+ _fig, axes = plt.subplots(1, 3, figsize=(11, 4))
+
+ # --- Panel 1: CNN local vs Transformer global ---
+ ax = axes[0]
+ ax.set_xlim(-0.5, 8.5)
+ ax.set_ylim(-1.5, 8.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("CNN: widzi LOKALNIE", fontsize=FS_TITLE, fontweight="bold")
+
+ # Draw 8x8 grid
+ for i in range(8):
+ for j in range(8):
+ color = WHITE
+ if 3 <= i <= 5 and 3 <= j <= 5:
+ color = ACCENT_LIGHT
+ rect = patches.Rectangle(
+ (j, 7 - i), 1, 1, facecolor=color, edgecolor=GRAY3, linewidth=0.3
+ )
+ ax.add_patch(rect)
+
+ # Highlight center
+ rect = patches.Rectangle(
+ (4, 4), 1, 1, facecolor=RED_ACCENT, edgecolor=BLACK, linewidth=1.5, alpha=0.7
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 4.5,
+ 4.5,
+ "?",
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ color=WHITE,
+ )
+ ax.text(
+ 4.5,
+ -0.8,
+ "Filtr 3x3 widzi tylko\n9 sąsiednich pikseli",
+ fontsize=FS_SMALL,
+ ha="center",
+ color=ACCENT,
+ )
+
+ # --- Panel 2: Transformer global ---
+ ax = axes[1]
+ ax.set_xlim(-0.5, 8.5)
+ ax.set_ylim(-1.5, 8.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Transformer: widzi GLOBALNIE", fontsize=FS_TITLE, fontweight="bold")
+
+ # Draw 8x8 grid all highlighted
+ for i in range(8):
+ for j in range(8):
+ color = "#FFCDD2"
+ rect = patches.Rectangle(
+ (j, 7 - i), 1, 1, facecolor=color, edgecolor=GRAY3, linewidth=0.3
+ )
+ ax.add_patch(rect)
+
+ rect = patches.Rectangle(
+ (4, 4), 1, 1, facecolor=RED_ACCENT, edgecolor=BLACK, linewidth=1.5, alpha=0.9
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 4.5,
+ 4.5,
+ "?",
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ color=WHITE,
+ )
+ ax.text(
+ 4.5,
+ -0.8,
+ 'Self-attention „pyta"\nALL 64 piksele naraz',
+ fontsize=FS_SMALL,
+ ha="center",
+ color=RED_ACCENT,
+ )
+
+ # --- Panel 3: SOTA + Transformer explanation ---
+ ax = axes[2]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title("Transformer & SOTA", fontsize=FS_TITLE, fontweight="bold")
+
+ y = 9.2
+ lines = [
+ ("Transformer:", FS, BLACK, "bold"),
+ (" Architektura z 2017 (Vaswani et al.)", FS_SMALL, BLACK, "normal"),
+ (" Oryginalnie do NLP (tłumaczenie)", FS_SMALL, BLACK, "normal"),
+ (" Kluczowy mechanizm: SELF-ATTENTION", FS_SMALL, ACCENT, "bold"),
+ ("", 0, "", ""),
+ ("Self-attention w skrócie:", FS, BLACK, "bold"),
+ (" Każdy piksel tworzy trzy wektory:", FS_SMALL, BLACK, "normal"),
+ (' Q (Query — „czego szukam?")', FS_SMALL, ACCENT, "normal"),
+ (' K (Key — „co oferuję innych")', FS_SMALL, RED_ACCENT, "normal"),
+ (' V (Value — „moja wartość")', FS_SMALL, GREEN_ACCENT, "normal"),
+ (" Attention = softmax(Q·Kᵀ/√d)·V", FS_SMALL, BLACK, "bold"),
+ (" Koszt: O(n²) — n=liczba pikseli", FS_SMALL, RED_ACCENT, "normal"),
+ ("", 0, "", ""),
+ ("SOTA = State Of The Art:", FS, BLACK, "bold"),
+ (" Najlepszy znany wynik na benchmarku", FS_SMALL, BLACK, "normal"),
+ (' Np. „mIoU 85.1% na ADE20K = SOTA"', FS_SMALL, BLACK, "normal"),
+ (" Ciągle się zmienia (nowy paper", FS_SMALL, GRAY5, "normal"),
+ (" → nowy SOTA)", FS_SMALL, GRAY5, "normal"),
+ ]
+ for txt, size, color, weight in lines:
+ if txt == "":
+ y -= 0.15
+ continue
+ ax.text(0.3, y, txt, fontsize=size, color=color, fontweight=weight, va="top")
+ y -= 0.45
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_transformer_attention.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_transformer_attention.png")
+
+
+# ============================================================
+# 11. REGION GROWING — seed selection + BFS
+# ============================================================
+def generate_region_growing() -> None:
+ """Generate region growing."""
+ _fig, axes = plt.subplots(1, 3, figsize=(11, 4.2))
+
+ # --- Panel 1: Manual vs automatic seed ---
+ ax = axes[0]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title("Seed: ręcznie vs automatycznie", fontsize=FS_TITLE, fontweight="bold")
+
+ y = 9.2
+ lines = [
+ ("Ręczny seed:", FS, ACCENT, "bold"),
+ (" Użytkownik klika na obraz", FS_SMALL, BLACK, "normal"),
+ (' → „tu jest obiekt, od tego zacznij"', FS_SMALL, BLACK, "normal"),
+ (" Użycie: segmentacja interaktywna", FS_SMALL, GRAY5, "normal"),
+ (" (np. Photoshop — magic wand tool)", FS_SMALL, GRAY5, "normal"),
+ ("", 0, "", ""),
+ ("Automatyczny seed:", FS, RED_ACCENT, "bold"),
+ (" 1. Histogram → lokalne maxima", FS_SMALL, BLACK, "normal"),
+ (" (najczęstsza jasność → seed)", FS_SMALL, GRAY5, "normal"),
+ (" 2. Grid: siatka co N pikseli", FS_SMALL, BLACK, "normal"),
+ (" (np. seed co 50 px → 100 seedów)", FS_SMALL, GRAY5, "normal"),
+ (" 3. Losowe próbkowanie", FS_SMALL, BLACK, "normal"),
+ (" 4. Ekstrema lokalne gradientu", FS_SMALL, BLACK, "normal"),
+ ("", 0, "", ""),
+ ("Dlaczego OR?", FS, GREEN_ACCENT, "bold"),
+ (" Ręczny → precyzyjny, ale wolny", FS_SMALL, BLACK, "normal"),
+ (" Auto → szybki, ale over-segmentation", FS_SMALL, BLACK, "normal"),
+ ]
+ for txt, size, color, weight in lines:
+ if txt == "":
+ y -= 0.15
+ continue
+ ax.text(0.3, y, txt, fontsize=size, color=color, fontweight=weight, va="top")
+ y -= 0.45
+
+ # --- Panel 2: Region growing step by step ---
+ ax = axes[1]
+ ax.set_xlim(-0.5, 6.5)
+ ax.set_ylim(-1.5, 7.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Region Growing: krok po kroku", fontsize=FS_TITLE, fontweight="bold")
+
+ # 6x6 grid with values
+ pixel_grid = np.array(
+ [
+ [150, 153, 148, 200, 210, 205],
+ [147, 155, 152, 195, 208, 200],
+ [145, 148, 160, 190, 195, 210],
+ [200, 195, 190, 155, 148, 150],
+ [210, 205, 200, 150, 152, 145],
+ [215, 208, 195, 148, 147, 155],
+ ]
+ )
+
+ # Region grown from seed (2,1) with threshold 20
+ region_mask = np.array(
+ [
+ [1, 1, 1, 0, 0, 0],
+ [1, 1, 1, 0, 0, 0],
+ [1, 1, 1, 0, 0, 0],
+ [0, 0, 0, 1, 1, 1],
+ [0, 0, 0, 1, 1, 1],
+ [0, 0, 0, 1, 1, 1],
+ ]
+ )
+
+ for i in range(6):
+ for j in range(6):
+ v = pixel_grid[i, j]
+ if region_mask[i, j] == 1 and v < 170:
+ color = ACCENT_LIGHT
+ elif region_mask[i, j] == 1:
+ color = GRAY2
+ else:
+ color = WHITE
+ if i == 1 and j == 1:
+ color = "#FFD54F" # Seed
+ rect = patches.Rectangle(
+ (j, 5 - i), 1, 1, facecolor=color, edgecolor=GRAY4, linewidth=0.5
+ )
+ ax.add_patch(rect)
+ ax.text(
+ j + 0.5,
+ 5 - i + 0.5,
+ str(v),
+ ha="center",
+ va="center",
+ fontsize=FS_TINY,
+ fontweight="bold",
+ )
+
+ # Mark seed
+ ax.annotate(
+ "SEED\n(155)",
+ xy=(1.5, 4.5),
+ fontsize=FS_SMALL,
+ ha="center",
+ color=RED_ACCENT,
+ fontweight="bold",
+ arrowprops={"arrowstyle": "->", "color": RED_ACCENT},
+ xytext=(-0.5, 7),
+ )
+
+ ax.text(
+ 3,
+ -0.8,
+ "Próg = 20\nNiebieski = region (|val - seed| < 20)",
+ fontsize=FS_TINY,
+ ha="center",
+ color=ACCENT,
+ )
+
+ # --- Panel 3: BFS expansion ---
+ ax = axes[2]
+ ax.set_xlim(-0.5, 6.5)
+ ax.set_ylim(-1.5, 7.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Rosnący region (BFS)", fontsize=FS_TITLE, fontweight="bold")
+
+ # Show expansion waves
+ wave_colors = ["#FFD54F", "#FFF176", "#FFF9C4", ACCENT_LIGHT, "#B3D4FC"]
+ wave_labels = ["Seed", "Fala 1", "Fala 2", "Fala 3", "Fala 4"]
+ waves = [
+ [(1, 1)], # seed
+ [(0, 1), (1, 0), (1, 2), (2, 1)], # wave 1
+ [(0, 0), (0, 2), (2, 0), (2, 2)], # wave 2
+ ]
+
+ for i in range(6):
+ for j in range(6):
+ color = WHITE
+ for w_idx, wave in enumerate(waves):
+ if (i, j) in wave:
+ color = wave_colors[w_idx]
+ rect = patches.Rectangle(
+ (j, 5 - i), 1, 1, facecolor=color, edgecolor=GRAY4, linewidth=0.5
+ )
+ ax.add_patch(rect)
+
+ # Draw BFS arrows from seed
+ seed_x, seed_y = 1.5, 4.5
+ for dx, dy, _label in [(0, 1, ""), (0, -1, ""), (1, 0, ""), (-1, 0, "")]:
+ ax.annotate(
+ "",
+ xy=(seed_x + dx * 0.7, seed_y + dy * 0.7),
+ xytext=(seed_x, seed_y),
+ arrowprops={"arrowstyle": "->", "color": RED_ACCENT, "lw": 1.2},
+ )
+
+ ax.text(
+ 3,
+ -0.5,
+ "BFS: sprawdzaj sąsiadów,\ndodawaj podobne do kolejki",
+ fontsize=FS_TINY,
+ ha="center",
+ color=GRAY5,
+ )
+
+ # Legend
+ for w_idx, (color, label) in enumerate(
+ zip(wave_colors[:3], wave_labels[:3], strict=False)
+ ):
+ rect = patches.Rectangle(
+ (4, 6.5 - w_idx * 0.7),
+ 0.5,
+ 0.5,
+ facecolor=color,
+ edgecolor=GRAY4,
+ linewidth=0.5,
+ )
+ ax.add_patch(rect)
+ ax.text(4.8, 6.75 - w_idx * 0.7, label, fontsize=FS_TINY, va="center")
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_region_growing.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_region_growing.png")
+
+
+# ============================================================
+# 12. DIY THRESHOLDING — Step-by-step example
+# ============================================================
+def generate_diy_thresholding() -> None:
+ """Generate diy thresholding."""
+ _fig, axes = plt.subplots(2, 3, figsize=(11, 7))
+
+ # Create a simple synthetic image: dark circle on bright background
+ size = 64
+ img = np.ones((size, size)) * 200 # bright background
+ yy, xx = np.mgrid[:size, :size]
+ mask = ((xx - 32) ** 2 + (yy - 32) ** 2) < 15**2
+ img[mask] = 60 # dark circle
+ # Add some noise
+ img += rng.normal(0, 10, img.shape)
+ img = np.clip(img, 0, 255)
+
+ # --- Panel 1: Original image ---
+ ax = axes[0, 0]
+ ax.imshow(img, cmap="gray", vmin=0, vmax=255)
+ ax.set_title("Krok 1: obraz wejściowy", fontsize=FS, fontweight="bold")
+ ax.axis("off")
+ ax.text(32, -3, "64x64 pikseli, szare", fontsize=FS_TINY, ha="center")
+
+ # --- Panel 2: Histogram ---
+ ax = axes[0, 1]
+ counts, _bins, _ = ax.hist(
+ img.ravel(), bins=50, color=GRAY3, edgecolor=GRAY5, linewidth=0.5
+ )
+ ax.axvline(
+ x=128, color=RED_ACCENT, linewidth=2, linestyle="--", label="T=128 (Otsu)"
+ )
+ ax.set_xlabel("Jasność", fontsize=FS_SMALL)
+ ax.set_ylabel("Piksele", fontsize=FS_SMALL)
+ ax.set_title("Krok 2: histogram\n(bimodalny!)", fontsize=FS, fontweight="bold")
+ ax.legend(fontsize=FS_TINY)
+ ax.annotate(
+ "Garb 1\n(obiekt)",
+ xy=(60, max(counts) * 0.5),
+ fontsize=FS_TINY,
+ ha="center",
+ color=ACCENT,
+ fontweight="bold",
+ )
+ ax.annotate(
+ "Garb 2\n(tło)",
+ xy=(200, max(counts) * 0.5),
+ fontsize=FS_TINY,
+ ha="center",
+ color=RED_ACCENT,
+ fontweight="bold",
+ )
+
+ # --- Panel 3: Thresholding result ---
+ ax = axes[0, 2]
+ binary = (img > 128).astype(float)
+ ax.imshow(binary, cmap="gray", vmin=0, vmax=1)
+ ax.set_title("Krok 3: progowanie T=128", fontsize=FS, fontweight="bold")
+ ax.axis("off")
+ ax.text(32, -3, "Biały = tło, Czarny = obiekt", fontsize=FS_TINY, ha="center")
+
+ # --- Panel 4: What Otsu does (variance plot) ---
+ ax = axes[1, 0]
+ # Compute within-class variance for each threshold
+ thresholds = range(10, 245)
+ variances = []
+ for t in thresholds:
+ c0 = img[img <= t].ravel()
+ c1 = img[img > t].ravel()
+ if len(c0) == 0 or len(c1) == 0:
+ variances.append(np.nan)
+ continue
+ w0 = len(c0) / len(img.ravel())
+ w1 = len(c1) / len(img.ravel())
+ var = w0 * np.var(c0) + w1 * np.var(c1)
+ variances.append(var)
+
+ ax.plot(list(thresholds), variances, color=ACCENT, linewidth=1.5)
+ best_t = list(thresholds)[np.nanargmin(variances)]
+ ax.axvline(
+ x=best_t,
+ color=RED_ACCENT,
+ linewidth=1.5,
+ linestyle="--",
+ label=f"Otsu T={best_t}",
+ )
+ ax.scatter([best_t], [np.nanmin(variances)], c=RED_ACCENT, s=60, zorder=5)
+ ax.set_xlabel("Próg T", fontsize=FS_SMALL)
+ ax.set_ylabel("σ² wewnątrzklasowa", fontsize=FS_SMALL)
+ ax.set_title("Krok 4: Otsu szuka min σ²", fontsize=FS, fontweight="bold")
+ ax.legend(fontsize=FS_TINY)
+
+ # --- Panel 5: Pseudocode ---
+ ax = axes[1, 1]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title("Pseudokod Otsu", fontsize=FS, fontweight="bold")
+
+ code_lines = [
+ "best_T = 0",
+ "min_var = ∞",
+ "",
+ "for T in 0..255:",
+ " c0 = piksele z jasność ≤ T",
+ " c1 = piksele z jasność > T",
+ " w0 = len(c0) / len(all)",
+ " w1 = len(c1) / len(all)",
+ " var = w0·var(c0) + w1·var(c1)",
+ " if var < min_var:",
+ " min_var = var",
+ " best_T = T",
+ "",
+ "return best_T # optymalny próg",
+ ]
+ for i, line in enumerate(code_lines):
+ color = ACCENT if "best_T = T" in line or "return" in line else BLACK
+ ax.text(
+ 0.5,
+ 9.5 - i * 0.65,
+ line,
+ fontsize=FS_TINY,
+ fontfamily="monospace",
+ color=color,
+ fontweight="bold" if color == ACCENT else "normal",
+ )
+
+ # --- Panel 6: Final result with Otsu ---
+ ax = axes[1, 2]
+ binary_otsu = (img > best_t).astype(float)
+ ax.imshow(binary_otsu, cmap="gray", vmin=0, vmax=1)
+ ax.set_title(f"Krok 5: wynik Otsu (T={best_t})", fontsize=FS, fontweight="bold")
+ ax.axis("off")
+ ax.text(
+ 32,
+ -3,
+ "Automatyczny próg!",
+ fontsize=FS_TINY,
+ ha="center",
+ color=GREEN_ACCENT,
+ fontweight="bold",
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_diy_thresholding.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_diy_thresholding.png")
+
+
+# ============================================================
+# 13. DIY U-NET — Simplified step-by-step
+# ============================================================
+def generate_diy_unet() -> None:
+ """Generate diy unet."""
+ fig, axes = plt.subplots(2, 3, figsize=(11, 7))
+
+ size = 64
+
+ # Create synthetic image with two regions
+ img = np.ones((size, size, 3), dtype=np.uint8) * 200 # bright bg
+ # Dark region (object 1)
+ yy, xx = np.mgrid[:size, :size]
+ mask1 = ((xx - 20) ** 2 + (yy - 30) ** 2) < 12**2
+ img[mask1] = [60, 60, 60]
+ # Medium region (object 2)
+ mask2 = ((xx - 45) ** 2 + (yy - 25) ** 2) < 8**2
+ img[mask2] = [120, 120, 120]
+
+ gt = np.zeros((size, size), dtype=np.uint8)
+ gt[mask1] = 1 # class 1
+ gt[mask2] = 2 # class 2
+
+ # --- Panel 1: Input image ---
+ ax = axes[0, 0]
+ ax.imshow(img)
+ ax.set_title("Krok 1: obraz RGB\n64x64x3", fontsize=FS, fontweight="bold")
+ ax.axis("off")
+
+ # --- Panel 2: Encoder shrinks ---
+ ax = axes[0, 1]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title("Krok 2: Encoder ZMNIEJSZA", fontsize=FS, fontweight="bold")
+
+ sizes = [(64, 3), (32, 64), (16, 128), (8, 256)]
+ y_pos = 8.5
+ for i, (s, c) in enumerate(sizes):
+ w = s / 64 * 4
+ h = 0.8
+ rect = FancyBboxPatch(
+ (5 - w / 2, y_pos),
+ w,
+ h,
+ boxstyle="round,pad=0.05",
+ facecolor=ACCENT_LIGHT,
+ edgecolor=ACCENT,
+ linewidth=1,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 5,
+ y_pos + h / 2,
+ f"{s}x{s}x{c}",
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ if i < len(sizes) - 1:
+ ax.annotate(
+ "",
+ xy=(5, y_pos - 0.3),
+ xytext=(5, y_pos),
+ arrowprops={"arrowstyle": "->", "color": ACCENT, "lw": 1.5},
+ )
+ ax.text(7, y_pos - 0.15, "Conv+Pool", fontsize=FS_TINY, color=ACCENT)
+ y_pos -= 2.2
+
+ ax.text(
+ 5,
+ 0.3,
+ "Wyciąga cechy:\nkrawędzie → tekstury → obiekty",
+ ha="center",
+ fontsize=FS_TINY,
+ color=GRAY5,
+ )
+
+ # --- Panel 3: Bottleneck ---
+ ax = axes[0, 2]
+ # Show feature maps at bottleneck (abstract)
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title(
+ "Krok 3: Bottleneck\n(najbardziej abstrakcyjne cechy)",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Show small abstract feature maps
+ for k in range(4):
+ small = rng.random((4, 4))
+ ax_inset = fig.add_axes(
+ [0.68 + (k % 2) * 0.08, 0.72 - (k // 2) * 0.1, 0.06, 0.06]
+ )
+ ax_inset.imshow(small, cmap="gray")
+ ax_inset.axis("off")
+
+ ax.text(
+ 5,
+ 5,
+ '8x8x256\n\nMałe mapy, ale DUŻO kanałów\nKażdy kanał = jedna „cecha"\n'
+ '(np. kanał 42 = „wykrył koło"\n kanał 78 = „wykrył krawędź")\n\n'
+ "Wie CO jest na obrazie\nale nie wie GDZIE dokładnie",
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ bbox={"boxstyle": "round", "facecolor": GRAY1, "edgecolor": GRAY3},
+ )
+
+ # --- Panel 4: Decoder enlarges ---
+ ax = axes[1, 0]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title(
+ "Krok 4: Decoder ZWIĘKSZA\n(+ skip connections!)",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ sizes_dec = [(8, 256), (16, 128), (32, 64), (64, 3)]
+ y_pos = 8.5
+ for i, (s, c) in enumerate(sizes_dec):
+ w = s / 64 * 4
+ h = 0.8
+ rect = FancyBboxPatch(
+ (5 - w / 2, y_pos),
+ w,
+ h,
+ boxstyle="round,pad=0.05",
+ facecolor="#C8E6C9",
+ edgecolor=GREEN_ACCENT,
+ linewidth=1,
+ )
+ ax.add_patch(rect)
+ label = f"{s}x{s}x{c}"
+ if i < len(sizes_dec) - 1:
+ label += " + skip!"
+ ax.text(
+ 5,
+ y_pos + h / 2,
+ label,
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ if i < len(sizes_dec) - 1:
+ ax.annotate(
+ "",
+ xy=(5, y_pos - 0.3),
+ xytext=(5, y_pos),
+ arrowprops={"arrowstyle": "->", "color": GREEN_ACCENT, "lw": 1.5},
+ )
+ ax.text(
+ 7, y_pos - 0.15, "UpConv+Concat", fontsize=FS_TINY, color=GREEN_ACCENT
+ )
+ y_pos -= 2.2
+
+ ax.text(
+ 5,
+ 0.3,
+ "Odtwarza rozdzielczość:\nskip → przywraca krawędzie",
+ ha="center",
+ fontsize=FS_TINY,
+ color=GRAY5,
+ )
+
+ # --- Panel 5: Output segmentation map ---
+ ax = axes[1, 1]
+ cmap = plt.cm.colors.ListedColormap([WHITE, ACCENT_LIGHT, "#FFCDD2"])
+ ax.imshow(gt, cmap=cmap, interpolation="nearest")
+ ax.set_title(
+ "Krok 5: mapa segmentacji\n64x64 (3 klasy)", fontsize=FS, fontweight="bold"
+ )
+ ax.axis("off")
+ ax.text(20, -3, "Tło=0, obiekt A=1, obiekt B=2", fontsize=FS_TINY, ha="center")
+
+ # --- Panel 6: Summary pseudocode ---
+ ax = axes[1, 2]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 10)
+ ax.axis("off")
+ ax.set_title("Pseudokod U-Net", fontsize=FS, fontweight="bold")
+
+ code_lines = [
+ "# ENCODER",
+ "e1 = conv_block(input, 64) # 64x64",
+ "e2 = conv_block(pool(e1), 128) # 32x32",
+ "e3 = conv_block(pool(e2), 256) # 16x16",
+ "",
+ "# BOTTLENECK",
+ "b = conv_block(pool(e3), 512) # 8x8",
+ "",
+ "# DECODER + SKIP",
+ "d3 = conv_block(concat(",
+ " upconv(b), e3), 256) # 16x16",
+ "d2 = conv_block(concat(",
+ " upconv(d3), e2), 128) # 32x32",
+ "d1 = conv_block(concat(",
+ " upconv(d2), e1), 64) # 64x64",
+ "",
+ "output = conv_1x1(d1, n_classes)",
+ ]
+ for i, line in enumerate(code_lines):
+ color = (
+ ACCENT
+ if "concat" in line
+ else (GREEN_ACCENT if "output" in line else BLACK)
+ )
+ ax.text(
+ 0.3,
+ 9.5 - i * 0.55,
+ line,
+ fontsize=FS_TINY,
+ fontfamily="monospace",
+ color=color,
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_diy_unet.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_diy_unet.png")
+
+
+# ============================================================
+# 14. MNEMONICS — Visual mnemonic summary
+# ============================================================
+def generate_mnemonics() -> None:
+ """Generate mnemonics."""
+ _fig, ax = plt.subplots(1, 1, figsize=(10, 8))
+ ax.set_xlim(0, 20)
+ ax.set_ylim(0, 16)
+ ax.axis("off")
+ ax.set_title(
+ "Mnemoniki — segmentacja obrazu", fontsize=FS_TITLE + 2, fontweight="bold"
+ )
+
+ def draw_card(ax, x, y, w, h, title, mnemonic, color, detail="") -> None:
+ """Draw card."""
+ rect = FancyBboxPatch(
+ (x, y),
+ w,
+ h,
+ boxstyle="round,pad=0.15",
+ facecolor=color,
+ edgecolor=BLACK,
+ linewidth=1,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h - 0.3,
+ title,
+ ha="center",
+ va="top",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.text(
+ x + w / 2,
+ y + h / 2 - 0.1,
+ mnemonic,
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ fontstyle="italic",
+ color=GRAY6,
+ )
+ if detail:
+ ax.text(
+ x + w / 2,
+ y + 0.4,
+ detail,
+ ha="center",
+ va="bottom",
+ fontsize=FS_TINY,
+ color=GRAY5,
+ )
+
+ # Title: STRATEGIE KLASYCZNE
+ ax.text(
+ 5,
+ 15.5,
+ "STRATEGIE KLASYCZNE",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ color=ACCENT,
+ ha="center",
+ )
+
+ cards_classic = [
+ (
+ 0.2,
+ 12.5,
+ 4.5,
+ 2.5,
+ "Thresholding",
+ '„PRÓG na bramce"\nPrzepuszcza > T,\nblokuje ≤ T',
+ ACCENT_LIGHT,
+ "jasne=1, ciemne=0",
+ ),
+ (
+ 5,
+ 12.5,
+ 4.5,
+ 2.5,
+ "Otsu",
+ '„AUTO-bramkarz"\nSam dobiera próg\nmin σ² wewnątrz',
+ ACCENT_LIGHT,
+ "histogram bimodalny",
+ ),
+ (
+ 0.2,
+ 9.5,
+ 4.5,
+ 2.5,
+ "Region Growing",
+ '„PLAMA rozlana"\nSeed → BFS po\npodobnych sąsiadach',
+ ACCENT_LIGHT,
+ "jak atrament na papierze",
+ ),
+ (
+ 5,
+ 9.5,
+ 4.5,
+ 2.5,
+ "Watershed",
+ '„ZALEWANIE terenu"\nDoliny=obiekty\nGranie=granice',
+ ACCENT_LIGHT,
+ "woda + geography",
+ ),
+ (
+ 0.2,
+ 6.5,
+ 4.5,
+ 2.5,
+ "Mean Shift",
+ '„KULKI toczą się"\nKażda → max gęstości\nBez K!',
+ ACCENT_LIGHT,
+ "bandwidth = okno",
+ ),
+ (
+ 5,
+ 6.5,
+ 4.5,
+ 2.5,
+ "Normalized Cuts",
+ '„CIĘCIE sznurków"\nGraf: tnij słabe\nkrawędzie (O(n³)!)',
+ ACCENT_LIGHT,
+ "eigenvector problem",
+ ),
+ ]
+
+ for args in cards_classic:
+ draw_card(ax, *args)
+
+ # Title: SIECI NEURONOWE
+ ax.text(
+ 15,
+ 15.5,
+ "SIECI NEURONOWE",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ color=GREEN_ACCENT,
+ ha="center",
+ )
+
+ cards_nn = [
+ (
+ 10.5,
+ 12.5,
+ 4.5,
+ 2.5,
+ "FCN (2015)",
+ '„FC → Conv 1x1"\nPierwsza end-to-end\nDowolny rozmiar',
+ "#C8E6C9",
+ "skip connections",
+ ),
+ (
+ 15.3,
+ 12.5,
+ 4.5,
+ 2.5,
+ "U-Net (2015)",
+ '„Litera U"\nEncoder↓ Decoder↑\nSkip = concat',
+ "#C8E6C9",
+ "medycyna, małe dane",
+ ),
+ (
+ 10.5,
+ 9.5,
+ 4.5,
+ 2.5,
+ "DeepLab v3+",
+ '„DZIURY w filtrze"\nAtrous conv (rate)\nASPP multi-scale',
+ "#C8E6C9",
+ "à trous = z dziurami",
+ ),
+ (
+ 15.3,
+ 9.5,
+ 4.5,
+ 2.5,
+ "Transformer",
+ '„WSZYSCY ze\nWSZYSTKIMI"\nSelf-attention O(n²)',
+ "#C8E6C9",
+ "SegFormer, Mask2Former",
+ ),
+ ]
+
+ for args in cards_nn:
+ draw_card(ax, *args)
+
+ # Metryki
+ ax.text(
+ 10,
+ 8.3,
+ "METRYKI I LOSS",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ color=RED_ACCENT,
+ ha="center",
+ )
+
+ cards_metrics = [
+ (
+ 10.5,
+ 6.5,
+ 4.5,
+ 1.6,
+ "mIoU",
+ '„Nakładka / Suma"\nIoU = A∩B / A\u222aB',
+ "#FFCDD2",
+ "",
+ ),
+ (
+ 15.3,
+ 6.5,
+ 4.5,
+ 1.6,
+ "Dice / Focal",
+ '„Dice=2·nakładka"\nFocal=trudne px',
+ "#FFCDD2",
+ "",
+ ),
+ ]
+
+ for args in cards_metrics:
+ draw_card(ax, *args)
+
+ # Master mnemonic at bottom
+ rect = FancyBboxPatch(
+ (1, 0.3),
+ 18,
+ 5.5,
+ boxstyle="round,pad=0.2",
+ facecolor=GRAY1,
+ edgecolor=BLACK,
+ linewidth=1.5,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 10,
+ 5.3,
+ "SUPER-MNEMONIK: kolejność algorytmów segmentacji",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.text(
+ 10,
+ 4.5,
+ '„TORW-MN FUD-T"',
+ ha="center",
+ fontsize=FS_TITLE + 2,
+ fontweight="bold",
+ color=RED_ACCENT,
+ )
+ ax.text(
+ 10,
+ 3.5,
+ "Klasyczne: Thresholding → Otsu → Region growing → Watershed → Mean shift → Norm. cuts",
+ ha="center",
+ fontsize=FS_SMALL,
+ )
+ ax.text(
+ 10,
+ 2.8,
+ "Neuronowe: FCN → U-Net → DeepLab → Transformer",
+ ha="center",
+ fontsize=FS_SMALL,
+ )
+ ax.text(
+ 10,
+ 1.8,
+ '„Turyści Oglądają Rzekę, Wodospad, Morze, Nurt — Fotografują Uroczy Dwór Tajemnic"',
+ ha="center",
+ fontsize=FS_SMALL,
+ fontstyle="italic",
+ color=ACCENT,
+ )
+ ax.text(
+ 10,
+ 1.0,
+ "Klasyczne: proste→auto→BFS→flood→gęstość→graf | Neuronowe: FC→U-skip→dilated→attention",
+ ha="center",
+ fontsize=FS_TINY,
+ color=GRAY5,
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "q23_mnemonics.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor="white",
+ )
+ plt.close()
+ print(" ✓ q23_mnemonics.png")
+
+
+# ============================================================
+# MAIN
+# ============================================================
+if __name__ == "__main__":
+ print("Generating PYTANIE 23 diagrams...")
+ generate_otsu_bimodal()
+ generate_watershed()
+ generate_mean_shift()
+ generate_normalized_cuts()
+ generate_relu()
+ generate_dot_product()
+ generate_fcn()
+ generate_unet()
+ generate_receptive_field()
+ generate_transformer()
+ generate_region_growing()
+ generate_diy_thresholding()
+ generate_diy_unet()
+ generate_mnemonics()
+ print(f"\nAll diagrams saved to: {OUTPUT_DIR}")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_q24_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_q24_diagrams.py
new file mode 100755
index 0000000..9640f43
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_q24_diagrams.py
@@ -0,0 +1,2270 @@
+#!/usr/bin/env python3
+"""Generate ALL diagrams for PYTANIE 24: Detekcja obiektów.
+
+Monochrome, A4-printable PNGs (300 DPI).
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+import numpy as np
+
+rng = np.random.default_rng(42)
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 11
+FS_SMALL = 6.5
+FS_LABEL = 9
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.2,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=True,
+ edgecolor=LN,
+ linestyle="-",
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y),
+ w,
+ h,
+ boxstyle="round,pad=0.05",
+ lw=lw,
+ edgecolor=edgecolor,
+ facecolor=fill,
+ linestyle=linestyle,
+ )
+ else:
+ rect = mpatches.Rectangle(
+ (x, y),
+ w,
+ h,
+ lw=lw,
+ edgecolor=edgecolor,
+ facecolor=fill,
+ linestyle=linestyle,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ )
+
+
+def save_fig(fig, name) -> None:
+ """Save fig."""
+ path = str(Path(OUTPUT_DIR) / name)
+ fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG, pad_inches=0.15)
+ plt.close(fig)
+ print(f" Saved: {path}")
+
+
+def draw_table(
+ ax,
+ headers,
+ rows,
+ x0,
+ y0,
+ col_widths,
+ row_h=0.4,
+ header_fill=GRAY2,
+ row_fills=None,
+ fontsize=FS,
+ header_fontsize=None,
+) -> None:
+ """Draw table."""
+ if header_fontsize is None:
+ header_fontsize = fontsize
+ len(headers)
+ cx = x0
+ for j, hdr in enumerate(headers):
+ draw_box(
+ ax,
+ cx,
+ y0,
+ col_widths[j],
+ row_h,
+ hdr,
+ fill=header_fill,
+ fontsize=header_fontsize,
+ fontweight="bold",
+ rounded=False,
+ )
+ cx += col_widths[j]
+ for i, row in enumerate(rows):
+ cy = y0 - (i + 1) * row_h
+ cx = x0
+ fill = GRAY4 if (i % 2 == 0) else "white"
+ if row_fills and i < len(row_fills):
+ fill = row_fills[i]
+ for j, cell in enumerate(row):
+ fw = "bold" if j == 0 else "normal"
+ draw_box(
+ ax,
+ cx,
+ cy,
+ col_widths[j],
+ row_h,
+ cell,
+ fill=fill,
+ fontsize=fontsize,
+ fontweight=fw,
+ rounded=False,
+ )
+ cx += col_widths[j]
+
+
+# ============================================================
+# 1. HOG + SVM Pipeline
+# ============================================================
+def draw_hog_svm_pipeline() -> None:
+ """Draw hog svm pipeline."""
+ fig, ax = plt.subplots(figsize=(10, 4.5))
+ ax.set_xlim(-0.5, 10.5)
+ ax.set_ylim(-1, 4.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "HOG + SVM — pipeline detekcji pieszych",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Step 1: Image with sliding window
+ ax.add_patch(
+ mpatches.Rectangle((0, 1.5), 2, 2, lw=1.5, edgecolor=LN, facecolor=GRAY1)
+ )
+ ax.text(1, 2.5, "Obraz\nwejściowy", ha="center", va="center", fontsize=FS)
+ # sliding window overlay
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0.3, 1.8),
+ 0.8,
+ 1.2,
+ lw=1.5,
+ edgecolor="black",
+ facecolor="none",
+ linestyle="--",
+ )
+ )
+ ax.text(
+ 0.7,
+ 1.35,
+ "okno 64x128",
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ style="italic",
+ )
+
+ draw_arrow(ax, 2.1, 2.5, 2.8, 2.5, lw=1.5)
+ ax.text(2.45, 2.75, "①", ha="center", fontsize=FS_LABEL, fontweight="bold")
+
+ # Step 2: Gradient computation
+ draw_box(
+ ax, 2.9, 1.8, 1.6, 1.4, "Oblicz\ngradienty\nGx, Gy", fill=GRAY4, fontsize=FS
+ )
+ ax.text(
+ 3.7, 1.55, "kierunek + siła", ha="center", fontsize=FS_SMALL, style="italic"
+ )
+
+ draw_arrow(ax, 4.6, 2.5, 5.2, 2.5, lw=1.5)
+ ax.text(4.9, 2.75, "②", ha="center", fontsize=FS_LABEL, fontweight="bold")
+
+ # Step 3: HOG histogram
+ draw_box(
+ ax,
+ 5.3,
+ 1.8,
+ 1.6,
+ 1.4,
+ "Histogramy\nkierunkowe\n9 binów/cel",
+ fill=GRAY4,
+ fontsize=FS,
+ )
+ ax.text(6.1, 1.55, "komórki 8x8 px", ha="center", fontsize=FS_SMALL, style="italic")
+
+ draw_arrow(ax, 7.0, 2.5, 7.6, 2.5, lw=1.5)
+ ax.text(7.3, 2.75, "③", ha="center", fontsize=FS_LABEL, fontweight="bold")
+
+ # Step 4: SVM
+ draw_box(
+ ax,
+ 7.7,
+ 1.8,
+ 1.4,
+ 1.4,
+ "SVM\nklasyfikator\npieszy/tło",
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ draw_arrow(ax, 9.2, 2.5, 9.7, 2.5, lw=1.5)
+ ax.text(9.45, 2.75, "④", ha="center", fontsize=FS_LABEL, fontweight="bold")
+
+ # Step 5: NMS + output
+ draw_box(ax, 9.3, 2.0, 1.0, 1.0, "NMS\n→ wynik", fill=GRAY1, fontsize=FS)
+
+ # Bottom: HOG feature vector illustration
+ ax.text(
+ 5.0,
+ 0.7,
+ "Wektor HOG: 3780 cech = 105 bloków x 4 komórki x 9 binów",
+ ha="center",
+ fontsize=FS,
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # Show small histogram bars
+ bar_x = 3.2
+ bar_y = 0.0
+ angles = [0, 20, 40, 60, 80, 100, 120, 140, 160]
+ values = [0.3, 0.1, 0.5, 0.8, 0.2, 0.6, 0.15, 0.4, 0.25]
+ for i, (_a, v) in enumerate(zip(angles, values, strict=False)):
+ ax.add_patch(
+ mpatches.Rectangle(
+ (bar_x + i * 0.18, bar_y),
+ 0.15,
+ v * 0.6,
+ facecolor=GRAY3,
+ edgecolor=LN,
+ lw=0.5,
+ )
+ )
+ ax.text(bar_x + 0.8, -0.2, "9 binów (0°-160°)", ha="center", fontsize=FS_SMALL)
+
+ save_fig(fig, "q24_hog_svm_pipeline.png")
+
+
+# ============================================================
+# 2. HOG Gradient Step-by-Step
+# ============================================================
+def draw_hog_gradient_steps() -> None:
+ """Draw hog gradient steps."""
+ fig, axes = plt.subplots(1, 4, figsize=(12, 3.5))
+ fig.suptitle(
+ "HOG — kroki obliczania cech", fontsize=FS_TITLE, fontweight="bold", y=1.02
+ )
+
+ # Step 1: Original patch
+ ax = axes[0]
+ patch = np.array([[50, 50, 200], [50, 50, 200], [50, 50, 200]])
+ ax.imshow(patch, cmap="gray", vmin=0, vmax=255)
+ for i in range(3):
+ for j in range(3):
+ ax.text(
+ j,
+ i,
+ str(patch[i, j]),
+ ha="center",
+ va="center",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ color="white" if patch[i, j] > 127 else "black",
+ )
+ ax.set_title("① Fragment obrazu\n(jasność pikseli)", fontsize=FS, fontweight="bold")
+ ax.set_xticks([])
+ ax.set_yticks([])
+
+ # Step 2: Gradient magnitude
+ ax = axes[1]
+ gx = np.array([[0, 150, 0], [0, 150, 0], [0, 150, 0]])
+ ax.imshow(gx, cmap="gray", vmin=0, vmax=255)
+ for i in range(3):
+ for j in range(3):
+ ax.text(
+ j,
+ i,
+ str(gx[i, j]),
+ ha="center",
+ va="center",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ color="white" if gx[i, j] > 100 else "black",
+ )
+ ax.set_title("② Gradient Gx\n(krawędź pionowa!)", fontsize=FS, fontweight="bold")
+ ax.set_xticks([])
+ ax.set_yticks([])
+
+ # Step 3: Cell histogram
+ ax = axes[2]
+ angles = ["0°", "20°", "40°", "60°", "80°", "100°", "120°", "140°", "160°"]
+ values = [150, 0, 0, 0, 0, 0, 0, 0, 0]
+ bars = ax.bar(range(9), values, color=GRAY3, edgecolor=LN, linewidth=0.5)
+ bars[0].set_facecolor(GRAY5)
+ ax.set_xticks(range(9))
+ ax.set_xticklabels(angles, fontsize=5, rotation=45)
+ ax.set_title(
+ "③ Histogram komórki\n(bin 0° = krawędź pionowa)",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.set_ylabel("siła", fontsize=FS_SMALL)
+
+ # Step 4: Block normalization
+ ax = axes[3]
+ # 2x2 block of cells
+ for i in range(2):
+ for j in range(2):
+ rect = mpatches.Rectangle(
+ (j * 1.2, (1 - i) * 1.2),
+ 1.0,
+ 1.0,
+ lw=1.2,
+ edgecolor=LN,
+ facecolor=GRAY4,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ j * 1.2 + 0.5,
+ (1 - i) * 1.2 + 0.5,
+ f"hist\n{i * 2 + j + 1}",
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ )
+ ax.add_patch(
+ mpatches.Rectangle(
+ (-0.1, -0.1), 2.6, 2.6, lw=2, edgecolor=LN, facecolor="none", linestyle="--"
+ )
+ )
+ ax.text(
+ 1.2,
+ -0.4,
+ "blok 2x2 → L2-norm",
+ ha="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ ax.set_xlim(-0.3, 2.8)
+ ax.set_ylim(-0.7, 2.8)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "④ Normalizacja bloków\n(odporność na oświetlenie)",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ fig.tight_layout()
+ save_fig(fig, "q24_hog_gradient_steps.png")
+
+
+# ============================================================
+# 3. Viola-Jones Cascade
+# ============================================================
+def draw_viola_jones_cascade() -> None:
+ """Draw viola jones cascade."""
+ fig, ax = plt.subplots(figsize=(10, 5))
+ ax.set_xlim(-0.5, 10.5)
+ ax.set_ylim(-1.5, 5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Viola-Jones — kaskada klasyfikatorów (SITO)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Input
+ draw_box(
+ ax,
+ -0.3,
+ 2.5,
+ 1.5,
+ 1.2,
+ "500 000\nokien",
+ fill=GRAY1,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ stages = [
+ ("Etap 1\n2 cechy", "50%\nodrzucone", "250 000", GRAY4),
+ ("Etap 2\n10 cech", "80%\nodrzucone", "50 000", GRAY4),
+ ("Etap 3\n25 cech", "90%\nodrzucone", "5 000", GRAY4),
+ ("Etap 25\n200 cech", "99%\nodrzucone", "50", GRAY3),
+ ]
+
+ x_pos = 1.6
+ for i, (label, reject, remain, col) in enumerate(stages):
+ # Stage box
+ draw_box(
+ ax, x_pos, 2.5, 1.6, 1.2, label, fill=col, fontsize=FS, fontweight="bold"
+ )
+
+ # Arrow from previous
+ draw_arrow(ax, x_pos - 0.3, 3.1, x_pos - 0.05, 3.1, lw=1.5)
+
+ # Reject arrow down
+ draw_arrow(ax, x_pos + 0.8, 2.45, x_pos + 0.8, 1.6, lw=1.2)
+ ax.text(
+ x_pos + 0.8,
+ 1.3,
+ reject,
+ ha="center",
+ fontsize=FS_SMALL,
+ color="black",
+ style="italic",
+ )
+ ax.text(
+ x_pos + 0.8,
+ 0.8,
+ "✗ NIE-TWARZ",
+ ha="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+
+ # Remaining count above
+ if i < len(stages) - 1:
+ ax.text(
+ x_pos + 2.0,
+ 3.9,
+ f"→ {remain}",
+ ha="center",
+ fontsize=FS_SMALL,
+ style="italic",
+ )
+
+ # Dots between stage 3 and stage 25
+ if i == 2:
+ ax.text(
+ x_pos + 2.0, 3.1, "· · ·", ha="center", fontsize=12, fontweight="bold"
+ )
+ x_pos += 2.5
+ else:
+ x_pos += 2.1
+
+ # Final output
+ draw_arrow(ax, x_pos + 0.3, 3.1, x_pos + 0.9, 3.1, lw=1.5)
+ draw_box(
+ ax,
+ x_pos + 0.5,
+ 2.5,
+ 1.3,
+ 1.2,
+ "~50\nTWARZE\n✓",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Timing info
+ ax.text(
+ 5.0,
+ -0.5,
+ "Czas: 99% okien odrzucone w etapach 1-3 (~5 μs każde)\n"
+ "Tylko 0.01% dochodzi do etapu 25 → cały obraz w ~30 ms = 30+ fps",
+ ha="center",
+ fontsize=FS,
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.4", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save_fig(fig, "q24_viola_jones_cascade.png")
+
+
+# ============================================================
+# 4. Haar Features
+# ============================================================
+def draw_haar_features() -> None:
+ """Draw haar features."""
+ fig, axes = plt.subplots(1, 4, figsize=(11, 3))
+ fig.suptitle(
+ "Cechy Haar — typy i zastosowanie na twarzy",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=1.02,
+ )
+
+ # Feature 1: Vertical edge
+ ax = axes[0]
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 1, 2, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ ax.add_patch(
+ mpatches.Rectangle((1, 0), 1, 2, facecolor=GRAY3, edgecolor=LN, lw=1.5)
+ )
+ ax.text(
+ 0.5, 1, "+Σ₁", ha="center", va="center", fontsize=FS_LABEL, fontweight="bold"
+ )
+ ax.text(
+ 1.5, 1, "-Σ₂", ha="center", va="center", fontsize=FS_LABEL, fontweight="bold"
+ )
+ ax.set_xlim(-0.2, 2.2)
+ ax.set_ylim(-0.5, 2.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Krawędź pionowa\nwartość = Σ₁ - Σ₂", fontsize=FS)
+
+ # Feature 2: Horizontal edge
+ ax = axes[1]
+ ax.add_patch(
+ mpatches.Rectangle((0, 1), 2, 1, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 2, 1, facecolor=GRAY3, edgecolor=LN, lw=1.5)
+ )
+ ax.text(
+ 1, 1.5, "+Σ₁", ha="center", va="center", fontsize=FS_LABEL, fontweight="bold"
+ )
+ ax.text(
+ 1, 0.5, "-Σ₂", ha="center", va="center", fontsize=FS_LABEL, fontweight="bold"
+ )
+ ax.set_xlim(-0.2, 2.2)
+ ax.set_ylim(-0.5, 2.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Krawędź pozioma\n(oczy vs czoło)", fontsize=FS)
+
+ # Feature 3: Three-rectangle (line)
+ ax = axes[2]
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 0.7, 2, facecolor=GRAY3, edgecolor=LN, lw=1.5)
+ )
+ ax.add_patch(
+ mpatches.Rectangle((0.7, 0), 0.7, 2, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ ax.add_patch(
+ mpatches.Rectangle((1.4, 0), 0.7, 2, facecolor=GRAY3, edgecolor=LN, lw=1.5)
+ )
+ ax.text(
+ 0.35, 1, "-Σ₁", ha="center", va="center", fontsize=FS_SMALL, fontweight="bold"
+ )
+ ax.text(
+ 1.05, 1, "+Σ₂", ha="center", va="center", fontsize=FS_SMALL, fontweight="bold"
+ )
+ ax.text(
+ 1.75, 1, "-Σ₃", ha="center", va="center", fontsize=FS_SMALL, fontweight="bold"
+ )
+ ax.set_xlim(-0.2, 2.3)
+ ax.set_ylim(-0.5, 2.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Linia (3 prostokąty)\n(nos vs policzki)", fontsize=FS)
+
+ # Feature 4: Application on face (schematic)
+ ax = axes[3]
+ # Draw face outline (oval)
+ face = mpatches.Ellipse((1.2, 1.2), 2.0, 2.4, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ ax.add_patch(face)
+ # Eyes (dark)
+ ax.add_patch(
+ mpatches.Ellipse((0.7, 1.6), 0.4, 0.2, facecolor=GRAY3, edgecolor=LN, lw=1)
+ )
+ ax.add_patch(
+ mpatches.Ellipse((1.7, 1.6), 0.4, 0.2, facecolor=GRAY3, edgecolor=LN, lw=1)
+ )
+ # Nose (light)
+ ax.plot([1.2, 1.1, 1.3], [1.3, 0.9, 0.9], color=LN, lw=1)
+ # Mouth
+ ax.plot([0.8, 1.0, 1.2, 1.4, 1.6], [0.55, 0.5, 0.55, 0.5, 0.55], color=LN, lw=1)
+ # Haar feature overlay on eyes
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0.3, 1.4), 1.8, 0.4, facecolor="none", edgecolor=LN, lw=2, linestyle="--"
+ )
+ )
+ ax.annotate(
+ "cechy Haar\n(oczy ciemne\nvs czoło jasne)",
+ xy=(1.2, 1.85),
+ xytext=(2.2, 2.3),
+ fontsize=FS_SMALL,
+ ha="center",
+ arrowprops={"arrowstyle": "->", "color": LN, "lw": 1},
+ )
+ ax.set_xlim(-0.2, 3.0)
+ ax.set_ylim(-0.2, 2.8)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Zastosowanie na twarzy", fontsize=FS)
+
+ fig.tight_layout()
+ save_fig(fig, "q24_haar_features.png")
+
+
+# ============================================================
+# 5. Integral Image
+# ============================================================
+def draw_integral_image() -> None:
+ """Draw integral image."""
+ fig, axes = plt.subplots(1, 3, figsize=(11, 3.5))
+ fig.suptitle(
+ "Integral Image — suma prostokąta w O(1)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=1.02,
+ )
+
+ # Original image
+ ax = axes[0]
+ data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
+ ax.imshow(data, cmap="gray", vmin=0, vmax=10)
+ for i in range(3):
+ for j in range(3):
+ ax.text(
+ j,
+ i,
+ str(data[i, j]),
+ ha="center",
+ va="center",
+ fontsize=12,
+ fontweight="bold",
+ color="white" if data[i, j] > 5 else "black",
+ )
+ ax.set_title("① Obraz oryginalny", fontsize=FS, fontweight="bold")
+ ax.set_xticks([])
+ ax.set_yticks([])
+
+ # Integral image
+ ax = axes[1]
+ ii = np.array([[1, 3, 6], [5, 12, 21], [12, 27, 45]])
+ ax.imshow(ii, cmap="gray", vmin=0, vmax=50)
+ for i in range(3):
+ for j in range(3):
+ ax.text(
+ j,
+ i,
+ str(ii[i, j]),
+ ha="center",
+ va="center",
+ fontsize=12,
+ fontweight="bold",
+ color="white" if ii[i, j] > 25 else "black",
+ )
+ ax.set_title("② Integral Image\n(sumy kumulatywne)", fontsize=FS, fontweight="bold")
+ ax.set_xticks([])
+ ax.set_yticks([])
+
+ # Formula illustration
+ ax = axes[2]
+ ax.axis("off")
+ ax.set_xlim(0, 4)
+ ax.set_ylim(0, 4)
+ # Draw rectangle
+ ax.add_patch(
+ mpatches.Rectangle((0.5, 0.5), 3, 3, facecolor="white", edgecolor=LN, lw=1)
+ )
+ ax.add_patch(
+ mpatches.Rectangle((1.5, 0.5), 2, 2, facecolor=GRAY3, edgecolor=LN, lw=2)
+ )
+ # Labels
+ ax.text(0.3, 3.7, "A", fontsize=12, fontweight="bold")
+ ax.text(3.6, 3.7, "B", fontsize=12, fontweight="bold")
+ ax.text(0.3, 0.3, "C", fontsize=12, fontweight="bold")
+ ax.text(3.6, 0.3, "D", fontsize=12, fontweight="bold")
+ ax.text(
+ 2.5,
+ 1.5,
+ "SZUKANA\nSUMA",
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.text(
+ 2.0,
+ -0.3,
+ "Suma = D - B - C + A\n= 4 odczyty → O(1) ZAWSZE!",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+ ax.set_title(
+ "③ Formuła: 4 odczyty\n= O(1) niezależnie od rozmiaru",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ fig.tight_layout()
+ save_fig(fig, "q24_integral_image.png")
+
+
+# ============================================================
+# 6. R-CNN Evolution
+# ============================================================
+def draw_rcnn_evolution() -> None:
+ """Draw rcnn evolution."""
+ fig, ax = plt.subplots(figsize=(11, 7))
+ ax.set_xlim(-0.5, 11)
+ ax.set_ylim(-0.5, 7.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Ewolucja R-CNN: od 50s do 0.2s na obraz",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ y_positions = [5.5, 3.0, 0.5]
+ labels = [
+ "R-CNN (2014) — 50 s/obraz",
+ "Fast R-CNN (2015) — 2 s/obraz",
+ "Faster R-CNN (2015) — 0.2 s/obraz",
+ ]
+
+ # R-CNN
+ y = y_positions[0]
+ ax.text(0, y + 1.3, labels[0], fontsize=FS_LABEL, fontweight="bold")
+ draw_box(ax, 0, y, 2, 0.9, "Selective\nSearch", fill=GRAY2, fontsize=FS)
+ draw_arrow(ax, 2.1, y + 0.45, 2.5, y + 0.45)
+ ax.text(2.3, y + 0.8, "~2000", ha="center", fontsize=FS_SMALL, style="italic")
+ draw_box(ax, 2.6, y, 1.5, 0.9, "Resize\n224x224", fill=GRAY4, fontsize=FS)
+ draw_arrow(ax, 4.2, y + 0.45, 4.6, y + 0.45)
+ draw_box(
+ ax, 4.7, y, 1.5, 0.9, "CNN\nx2000!", fill=GRAY3, fontsize=FS, fontweight="bold"
+ )
+ draw_arrow(ax, 6.3, y + 0.45, 6.7, y + 0.45)
+ draw_box(ax, 6.8, y, 1.3, 0.9, "SVM\nklasyf.", fill=GRAY4, fontsize=FS)
+ draw_arrow(ax, 8.2, y + 0.45, 8.6, y + 0.45)
+ draw_box(ax, 8.7, y, 1.0, 0.9, "NMS", fill=GRAY1, fontsize=FS)
+ # Problem annotation
+ ax.text(
+ 5.5,
+ y - 0.4,
+ "⚠ CNN uruchamiane 2000x → 50 sek!",
+ ha="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # Fast R-CNN
+ y = y_positions[1]
+ ax.text(0, y + 1.3, labels[1], fontsize=FS_LABEL, fontweight="bold")
+ draw_box(ax, 0, y, 2, 0.9, "Selective\nSearch", fill=GRAY2, fontsize=FS)
+ draw_arrow(ax, 2.1, y + 0.45, 2.5, y + 0.45)
+ draw_box(
+ ax,
+ 2.6,
+ y,
+ 1.5,
+ 0.9,
+ "CNN\nx1 (RAZ!)",
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 4.2, y + 0.45, 4.6, y + 0.45)
+ draw_box(
+ ax, 4.7, y, 1.5, 0.9, "ROI\nPooling", fill=GRAY1, fontsize=FS, fontweight="bold"
+ )
+ draw_arrow(ax, 6.3, y + 0.45, 6.7, y + 0.45)
+ draw_box(ax, 6.8, y, 1.3, 0.9, "FC\nklasa+bbox", fill=GRAY4, fontsize=FS)
+ draw_arrow(ax, 8.2, y + 0.45, 8.6, y + 0.45)
+ draw_box(ax, 8.7, y, 1.0, 0.9, "NMS", fill=GRAY1, fontsize=FS)
+ ax.text(
+ 3.8,
+ y - 0.4,
+ "✓ CNN RAZ na cały obraz → 25x szybciej",
+ ha="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # Faster R-CNN
+ y = y_positions[2]
+ ax.text(0, y + 1.3, labels[2], fontsize=FS_LABEL, fontweight="bold")
+ draw_box(
+ ax,
+ 0.5,
+ y,
+ 1.5,
+ 0.9,
+ "CNN\nBackbone",
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 2.1, y + 0.45, 2.5, y + 0.45)
+ draw_box(ax, 2.6, y, 1.5, 0.9, "Feature\nMap", fill=GRAY1, fontsize=FS)
+ draw_arrow(ax, 4.2, y + 0.45, 4.6, y + 0.45)
+ draw_box(
+ ax,
+ 4.7,
+ y,
+ 1.3,
+ 0.9,
+ "RPN\n(w sieci!)",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 6.1, y + 0.45, 6.5, y + 0.45)
+ draw_box(ax, 6.6, y, 1.3, 0.9, "ROI\nPooling", fill=GRAY1, fontsize=FS)
+ draw_arrow(ax, 8.0, y + 0.45, 8.4, y + 0.45)
+ draw_box(ax, 8.5, y, 1.3, 0.9, "FC\nklasa+bbox", fill=GRAY4, fontsize=FS)
+ ax.text(
+ 5.0,
+ y - 0.4,
+ "✓ RPN zastępuje Selective Search → end-to-end",
+ ha="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save_fig(fig, "q24_rcnn_evolution.png")
+
+
+# ============================================================
+# 7. YOLO Grid
+# ============================================================
+def draw_yolo_grid() -> None:
+ """Draw yolo grid."""
+ fig, axes = plt.subplots(1, 3, figsize=(12, 4))
+ fig.suptitle(
+ "YOLO — detekcja jednoetapowa (siatka SxS)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=1.02,
+ )
+
+ # Grid on image
+ ax = axes[0]
+ S = 7
+ ax.set_xlim(0, S)
+ ax.set_ylim(0, S)
+ for i in range(S + 1):
+ ax.axhline(y=i, color=LN, lw=0.5, alpha=0.5)
+ ax.axvline(x=i, color=LN, lw=0.5, alpha=0.5)
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), S, S, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ # Highlight one cell
+ ax.add_patch(mpatches.Rectangle((3, 3), 1, 1, facecolor=GRAY2, edgecolor=LN, lw=2))
+ # Object center dot
+ ax.plot(3.5, 3.5, "ko", markersize=8)
+ # Bounding box from that cell
+ ax.add_patch(
+ mpatches.Rectangle(
+ (2.0, 2.2), 3.0, 2.6, facecolor="none", edgecolor=LN, lw=2, linestyle="--"
+ )
+ )
+ ax.text(
+ 3.5,
+ 1.8,
+ "bbox z komórki (3,3)",
+ ha="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ ax.set_aspect("equal")
+ ax.invert_yaxis()
+ ax.set_title("① Siatka 7x7\nna obrazie", fontsize=FS, fontweight="bold")
+ ax.set_xticks([])
+ ax.set_yticks([])
+
+ # Cell prediction
+ ax = axes[1]
+ ax.axis("off")
+ ax.set_xlim(0, 6)
+ ax.set_ylim(-1, 5)
+
+ # Draw prediction vector
+ labels = [
+ "x",
+ "y",
+ "w",
+ "h",
+ "conf",
+ "x",
+ "y",
+ "w",
+ "h",
+ "conf",
+ "P(c₁)",
+ "...",
+ "P(c₂₀)",
+ ]
+ colors_vec = [GRAY4] * 5 + [GRAY2] * 5 + [GRAY1] * 3
+
+ bw = 0.42
+ for i, (l, c) in enumerate(zip(labels, colors_vec, strict=False)):
+ x_pos = 0.3 + i * bw
+ ax.add_patch(
+ mpatches.Rectangle(
+ (x_pos, 2.5), bw - 0.02, 0.6, facecolor=c, edgecolor=LN, lw=0.8
+ )
+ )
+ ax.text(
+ x_pos + bw / 2,
+ 2.8,
+ l,
+ ha="center",
+ va="center",
+ fontsize=5,
+ fontweight="bold",
+ )
+
+ # Brackets for grouping
+ ax.annotate(
+ "", xy=(0.3, 2.4), xytext=(2.4, 2.4), arrowprops={"arrowstyle": "-", "lw": 1}
+ )
+ ax.text(1.35, 2.15, "bbox 1 (5 wartości)", ha="center", fontsize=FS_SMALL)
+
+ ax.annotate(
+ "", xy=(2.4, 2.4), xytext=(4.5, 2.4), arrowprops={"arrowstyle": "-", "lw": 1}
+ )
+ ax.text(3.45, 2.15, "bbox 2 (5 wartości)", ha="center", fontsize=FS_SMALL)
+
+ ax.annotate(
+ "", xy=(4.5, 2.4), xytext=(5.8, 2.4), arrowprops={"arrowstyle": "-", "lw": 1}
+ )
+ ax.text(5.15, 2.15, "20 klas", ha="center", fontsize=FS_SMALL)
+
+ ax.text(
+ 3.0,
+ 3.5,
+ "Każda komórka → 30 wartości\n= 2x(x,y,w,h,conf) + 20 klas",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ ax.set_title(
+ "② Predykcja jednej komórki\n(S=7, B=2, C=20)", fontsize=FS, fontweight="bold"
+ )
+
+ # Speed comparison
+ ax = axes[2]
+ ax.axis("off")
+ ax.set_xlim(0, 5)
+ ax.set_ylim(0, 5)
+
+ methods = ["R-CNN", "Fast R-CNN", "Faster R-CNN", "YOLO", "YOLOv8"]
+ fps_vals = [0.02, 0.5, 5, 45, 100]
+ bar_colors = [GRAY3, GRAY3, GRAY3, GRAY2, GRAY1]
+
+ for i, (m, f, c) in enumerate(zip(methods, fps_vals, bar_colors, strict=False)):
+ bar_w = f / 100 * 4.0
+ y_pos = 4.0 - i * 0.8
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0.5, y_pos), max(bar_w, 0.1), 0.5, facecolor=c, edgecolor=LN, lw=0.8
+ )
+ )
+ ax.text(
+ 0.4,
+ y_pos + 0.25,
+ m,
+ ha="right",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.text(
+ max(0.7, 0.5 + bar_w + 0.1),
+ y_pos + 0.25,
+ f"{f} fps",
+ ha="left",
+ va="center",
+ fontsize=FS,
+ )
+
+ ax.set_title(
+ "③ Porównanie szybkości\n(fps = klatki/sek)", fontsize=FS, fontweight="bold"
+ )
+
+ fig.tight_layout()
+ save_fig(fig, "q24_yolo_grid.png")
+
+
+# ============================================================
+# 8. IoU Diagram
+# ============================================================
+def draw_iou_diagram() -> None:
+ """Draw iou diagram."""
+ fig, axes = plt.subplots(1, 3, figsize=(11, 3.5))
+ fig.suptitle(
+ "IoU (Intersection over Union) — miara nakładania bboxów",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=1.02,
+ )
+
+ # Low IoU
+ ax = axes[0]
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0, 0), 3, 3, facecolor=GRAY4, edgecolor=LN, lw=1.5, label="A"
+ )
+ )
+ ax.add_patch(
+ mpatches.Rectangle(
+ (2.5, 2.5),
+ 3,
+ 3,
+ facecolor=GRAY2,
+ edgecolor=LN,
+ lw=1.5,
+ alpha=0.7,
+ label="B",
+ )
+ )
+ # Intersection
+ ax.add_patch(
+ mpatches.Rectangle((2.5, 2.5), 0.5, 0.5, facecolor=GRAY3, edgecolor=LN, lw=2)
+ )
+ ax.text(1.5, 1.5, "A", ha="center", va="center", fontsize=12, fontweight="bold")
+ ax.text(4, 4, "B", ha="center", va="center", fontsize=12, fontweight="bold")
+ ax.set_xlim(-0.5, 6)
+ ax.set_ylim(-0.5, 6)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "IoU ≈ 0.04\n(prawie się nie nakładają)", fontsize=FS, fontweight="bold"
+ )
+
+ # Medium IoU
+ ax = axes[1]
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 3, 3, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ ax.add_patch(
+ mpatches.Rectangle(
+ (1.5, 1.5), 3, 3, facecolor=GRAY2, edgecolor=LN, lw=1.5, alpha=0.7
+ )
+ )
+ ax.add_patch(
+ mpatches.Rectangle((1.5, 1.5), 1.5, 1.5, facecolor=GRAY3, edgecolor=LN, lw=2)
+ )
+ ax.text(0.7, 0.7, "A", ha="center", va="center", fontsize=12, fontweight="bold")
+ ax.text(3.5, 3.5, "B", ha="center", va="center", fontsize=12, fontweight="bold")
+ ax.text(2.25, 2.25, "∩", ha="center", va="center", fontsize=14, fontweight="bold")
+ ax.set_xlim(-0.5, 5)
+ ax.set_ylim(-0.5, 5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("IoU ≈ 0.14\n(częściowe nakładanie)", fontsize=FS, fontweight="bold")
+
+ # High IoU
+ ax = axes[2]
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 3, 3, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0.3, 0.3), 3, 3, facecolor=GRAY2, edgecolor=LN, lw=1.5, alpha=0.7
+ )
+ )
+ ax.add_patch(
+ mpatches.Rectangle((0.3, 0.3), 2.7, 2.7, facecolor=GRAY3, edgecolor=LN, lw=2)
+ )
+ ax.text(-0.3, -0.3, "A", ha="center", va="center", fontsize=12, fontweight="bold")
+ ax.text(3.5, 3.5, "B", ha="center", va="center", fontsize=12, fontweight="bold")
+ ax.text(1.65, 1.65, "∩", ha="center", va="center", fontsize=14, fontweight="bold")
+ ax.set_xlim(-0.8, 4)
+ ax.set_ylim(-0.8, 4)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "IoU ≈ 0.74\n(duże nakładanie → duplikat!)", fontsize=FS, fontweight="bold"
+ )
+
+ fig.tight_layout()
+ save_fig(fig, "q24_iou_diagram.png")
+
+
+# ============================================================
+# 9. NMS Step-by-Step
+# ============================================================
+def draw_nms_steps() -> None:
+ """Draw nms steps."""
+ fig, axes = plt.subplots(1, 3, figsize=(12, 4))
+ fig.suptitle(
+ "NMS (Non-Maximum Suppression) — usuwanie duplikatów",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=1.02,
+ )
+
+ # Before NMS
+ ax = axes[0]
+ ax.add_patch(mpatches.Rectangle((0, 0), 6, 5, facecolor=GRAY4, edgecolor=LN, lw=1))
+ # Multiple overlapping boxes for same object
+ ax.add_patch(
+ mpatches.Rectangle((1, 1), 2.5, 3, facecolor="none", edgecolor=LN, lw=2)
+ )
+ ax.text(2.25, 4.2, "conf=0.95", ha="center", fontsize=FS_SMALL, fontweight="bold")
+ ax.add_patch(
+ mpatches.Rectangle(
+ (1.2, 1.3), 2.3, 2.8, facecolor="none", edgecolor=LN, lw=1.5, linestyle="--"
+ )
+ )
+ ax.text(2.35, 1.1, "conf=0.90", ha="center", fontsize=FS_SMALL)
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0.8, 0.8), 2.7, 3.2, facecolor="none", edgecolor=LN, lw=1, linestyle=":"
+ )
+ )
+ ax.text(2.15, 0.6, "conf=0.85", ha="center", fontsize=FS_SMALL)
+ # Different object
+ ax.add_patch(
+ mpatches.Rectangle((4, 2), 1.5, 1.5, facecolor="none", edgecolor=LN, lw=1.5)
+ )
+ ax.text(4.75, 3.7, "conf=0.80", ha="center", fontsize=FS_SMALL)
+ ax.text(
+ 2,
+ 0.2,
+ "⚠ 4 detekcje (3 duplikaty!)",
+ ha="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ ax.set_xlim(-0.3, 6.3)
+ ax.set_ylim(-0.3, 5.3)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "① Przed NMS\n(wiele nakładających się)", fontsize=FS, fontweight="bold"
+ )
+
+ # NMS process
+ ax = axes[1]
+ ax.axis("off")
+ ax.set_xlim(0, 6)
+ ax.set_ylim(0, 5)
+
+ steps = [
+ ("1. Sortuj: [0.95, 0.90, 0.85, 0.80]", 4.5),
+ ("2. Weź najlepszą (0.95) → ZACHOWAJ", 3.7),
+ ("3. IoU(0.95, 0.90)=0.82 > 0.5 → USUŃ", 2.9),
+ ("4. IoU(0.95, 0.85)=0.75 > 0.5 → USUŃ", 2.1),
+ ("5. IoU(0.95, 0.80)=0.10 < 0.5 → ZACHOWAJ", 1.3),
+ ]
+ colors = [GRAY4, GRAY2, GRAY4, GRAY4, GRAY2]
+ for (text, yp), c in zip(steps, colors, strict=False):
+ ax.text(
+ 3.0,
+ yp,
+ text,
+ ha="center",
+ fontsize=FS,
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": c, "edgecolor": GRAY3},
+ )
+
+ ax.set_title("② Algorytm NMS\n(próg IoU = 0.5)", fontsize=FS, fontweight="bold")
+
+ # After NMS
+ ax = axes[2]
+ ax.add_patch(mpatches.Rectangle((0, 0), 6, 5, facecolor=GRAY4, edgecolor=LN, lw=1))
+ # Only best box for each object
+ ax.add_patch(
+ mpatches.Rectangle((1, 1), 2.5, 3, facecolor="none", edgecolor=LN, lw=2.5)
+ )
+ ax.text(2.25, 4.2, "conf=0.95 ✓", ha="center", fontsize=FS_SMALL, fontweight="bold")
+ ax.add_patch(
+ mpatches.Rectangle((4, 2), 1.5, 1.5, facecolor="none", edgecolor=LN, lw=2.5)
+ )
+ ax.text(4.75, 3.7, "conf=0.80 ✓", ha="center", fontsize=FS_SMALL, fontweight="bold")
+ ax.text(
+ 3,
+ 0.2,
+ "✓ 2 unikalne obiekty",
+ ha="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ ax.set_xlim(-0.3, 6.3)
+ ax.set_ylim(-0.3, 5.3)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("③ Po NMS\n(1 bbox na obiekt)", fontsize=FS, fontweight="bold")
+
+ fig.tight_layout()
+ save_fig(fig, "q24_nms_steps.png")
+
+
+# ============================================================
+# 10. Detector from Classifier — 3 approaches
+# ============================================================
+def draw_detector_from_classifier() -> None:
+ """Draw detector from classifier."""
+ fig, ax = plt.subplots(figsize=(11, 9))
+ ax.set_xlim(-0.5, 11)
+ ax.set_ylim(-1, 9.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Jak zbudować detektor z klasyfikatora? — 3 podejścia",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # ---- Approach 1: Sliding Window ----
+ y = 7.0
+ ax.text(
+ 0,
+ y + 1.5,
+ "① Sliding Window (NAJWOLNIEJSZE)",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # Image with sliding window
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0, y - 0.6), 1.8, 1.8, facecolor=GRAY1, edgecolor=LN, lw=1.5
+ )
+ )
+ ax.text(0.9, y + 0.3, "obraz", ha="center", fontsize=FS_SMALL)
+ # Sliding windows
+ for dx, dy in [(0.1, 0.1), (0.4, 0.1), (0.7, 0.1), (0.1, 0.5), (0.4, 0.5)]:
+ ax.add_patch(
+ mpatches.Rectangle(
+ (dx, y - 0.5 + dy),
+ 0.5,
+ 0.5,
+ facecolor="none",
+ edgecolor=LN,
+ lw=0.8,
+ linestyle="--",
+ )
+ )
+
+ draw_arrow(ax, 2.0, y + 0.3, 2.7, y + 0.3, lw=1.2)
+ ax.text(2.35, y + 0.6, "xmiliony", fontsize=FS_SMALL, style="italic")
+
+ draw_box(
+ ax,
+ 2.8,
+ y - 0.3,
+ 1.8,
+ 1.2,
+ 'Klasyfikator\n(ResNet)\n"kot? pies? tło?"',
+ fill=GRAY4,
+ fontsize=FS,
+ )
+ draw_arrow(ax, 4.7, y + 0.3, 5.3, y + 0.3, lw=1.2)
+ draw_box(ax, 5.4, y - 0.3, 1.2, 1.2, "NMS", fill=GRAY1, fontsize=FS)
+ draw_arrow(ax, 6.7, y + 0.3, 7.3, y + 0.3, lw=1.2)
+ ax.text(
+ 8.5,
+ y + 0.3,
+ "~3.3h / obraz!\n⚠ NIEPRAKTYCZNE",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # ---- Approach 2: Region Proposals ----
+ y = 3.8
+ ax.text(
+ 0,
+ y + 1.5,
+ "② Region Proposals + Klasyfikator (= R-CNN)",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0, y - 0.6), 1.8, 1.8, facecolor=GRAY1, edgecolor=LN, lw=1.5
+ )
+ )
+ ax.text(0.9, y + 0.3, "obraz", ha="center", fontsize=FS_SMALL)
+ # A few smart regions
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0.1, y - 0.4), 0.7, 0.9, facecolor="none", edgecolor=LN, lw=1.5
+ )
+ )
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0.9, y + 0.0), 0.7, 0.6, facecolor="none", edgecolor=LN, lw=1.5
+ )
+ )
+
+ draw_arrow(ax, 2.0, y + 0.3, 2.7, y + 0.3, lw=1.2)
+ draw_box(
+ ax,
+ 2.8,
+ y - 0.3,
+ 1.6,
+ 1.2,
+ "Selective\nSearch\n~2000 regionów",
+ fill=GRAY2,
+ fontsize=FS,
+ )
+ draw_arrow(ax, 4.5, y + 0.3, 5.1, y + 0.3, lw=1.2)
+ ax.text(4.8, y + 0.6, "x2000", fontsize=FS_SMALL, style="italic")
+ draw_box(ax, 5.2, y - 0.3, 1.5, 1.2, "Klasyfikator\n(CNN)", fill=GRAY4, fontsize=FS)
+ draw_arrow(ax, 6.8, y + 0.3, 7.4, y + 0.3, lw=1.2)
+ draw_box(ax, 7.5, y - 0.3, 1.0, 1.2, "NMS", fill=GRAY1, fontsize=FS)
+ draw_arrow(ax, 8.6, y + 0.3, 9.0, y + 0.3, lw=1.2)
+ ax.text(
+ 10.0,
+ y + 0.3,
+ "~20-50 s/obraz\n(250x szybciej)",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # ---- Approach 3: Fine-tune backbone ----
+ y = 0.5
+ ax.text(
+ 0,
+ y + 1.5,
+ "③ Fine-tune backbone + detection head (NAJLEPSZE)",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY2, "edgecolor": GRAY3},
+ )
+
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0, y - 0.6), 1.8, 1.8, facecolor=GRAY1, edgecolor=LN, lw=1.5
+ )
+ )
+ ax.text(0.9, y + 0.3, "obraz", ha="center", fontsize=FS_SMALL)
+
+ draw_arrow(ax, 2.0, y + 0.3, 2.7, y + 0.3, lw=1.2)
+ draw_box(
+ ax,
+ 2.8,
+ y - 0.3,
+ 1.8,
+ 1.2,
+ "Pretrained\nbackbone\n(ResNet)",
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 4.7, y + 0.3, 5.3, y + 0.3, lw=1.2)
+
+ # Two heads from feature map
+ draw_box(ax, 5.4, y + 0.3, 1.6, 0.6, "cls head\nP(klasa)", fill=GRAY4, fontsize=FS)
+ draw_box(
+ ax, 5.4, y - 0.5, 1.6, 0.6, "bbox head\nΔx,Δy,Δw,Δh", fill=GRAY4, fontsize=FS
+ )
+
+ draw_arrow(ax, 7.1, y + 0.6, 7.7, y + 0.6, lw=1.0)
+ draw_arrow(ax, 7.1, y - 0.2, 7.7, y - 0.2, lw=1.0)
+ draw_box(ax, 7.8, y - 0.3, 1.0, 1.2, "NMS", fill=GRAY1, fontsize=FS)
+
+ draw_arrow(ax, 8.9, y + 0.3, 9.3, y + 0.3, lw=1.2)
+ ax.text(
+ 10.2,
+ y + 0.3,
+ "5-155 fps!\n✓ NAJLEPSZE",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY2, "edgecolor": GRAY3},
+ )
+
+ save_fig(fig, "q24_detector_from_classifier.png")
+
+
+# ============================================================
+# 11. SVM Hyperplane
+# ============================================================
+def draw_svm_hyperplane() -> None:
+ """Draw svm hyperplane."""
+ fig, ax = plt.subplots(figsize=(6, 5))
+ ax.set_title(
+ "SVM — hiperpłaszczyzna i margines",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Class +1 (top-right)
+ x_pos = rng.standard_normal(15) * 0.5 + 3
+ y_pos = rng.standard_normal(15) * 0.5 + 3
+ ax.scatter(
+ x_pos,
+ y_pos,
+ marker="o",
+ s=50,
+ facecolors="white",
+ edgecolors=LN,
+ linewidths=1.5,
+ label="klasa +1 (pieszy)",
+ zorder=3,
+ )
+
+ # Class -1 (bottom-left)
+ x_neg = rng.standard_normal(15) * 0.5 + 1
+ y_neg = rng.standard_normal(15) * 0.5 + 1
+ ax.scatter(
+ x_neg,
+ y_neg,
+ marker="x",
+ s=50,
+ c=LN,
+ linewidths=1.5,
+ label="klasa -1 (tło)",
+ zorder=3,
+ )
+
+ # Hyperplane (decision boundary)
+ x_line = np.linspace(-0.5, 5, 100)
+ y_line = -x_line + 4.0
+ ax.plot(x_line, y_line, "k-", lw=2, label="hiperpłaszczyzna")
+
+ # Margin lines
+ ax.plot(x_line, y_line + 0.7, "k--", lw=1, alpha=0.5)
+ ax.plot(x_line, y_line - 0.7, "k--", lw=1, alpha=0.5)
+
+ # Margin annotation
+ ax.annotate(
+ "",
+ xy=(2.5, 1.5 + 0.7),
+ xytext=(2.5, 1.5 - 0.7),
+ arrowprops={"arrowstyle": "<->", "color": LN, "lw": 1.5},
+ )
+ ax.text(2.8, 1.5, "margines\n(MAX!)", fontsize=FS, fontweight="bold")
+
+ # Support vectors (highlight closest points)
+ # Find points closest to the line
+ ax.scatter(
+ [2.5],
+ [2.2],
+ marker="o",
+ s=120,
+ facecolors="none",
+ edgecolors=LN,
+ linewidths=2.5,
+ zorder=4,
+ )
+ ax.scatter([1.5], [1.8], marker="x", s=120, c=LN, linewidths=2.5, zorder=4)
+ ax.annotate(
+ "support\nvectors",
+ xy=(1.5, 1.8),
+ xytext=(0.2, 3.0),
+ fontsize=FS,
+ fontweight="bold",
+ arrowprops={"arrowstyle": "->", "color": LN, "lw": 1},
+ )
+
+ ax.set_xlim(-0.5, 5)
+ ax.set_ylim(-0.5, 5)
+ ax.set_xlabel("cecha 1 (np. gradient pionowy)", fontsize=FS)
+ ax.set_ylabel("cecha 2 (np. gradient poziomy)", fontsize=FS)
+ ax.legend(fontsize=FS_SMALL, loc="lower right")
+ ax.set_aspect("equal")
+
+ save_fig(fig, "q24_svm_hyperplane.png")
+
+
+# ============================================================
+# 12. Two-stage vs One-stage comparison table
+# ============================================================
+def draw_two_vs_one_stage() -> None:
+ """Draw two vs one stage."""
+ fig, ax = plt.subplots(figsize=(10, 3.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(-0.5, 4.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Two-stage vs One-stage — porównanie",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=8,
+ )
+
+ headers = ["Cecha", "Two-stage\n(Faster R-CNN)", "One-stage\n(YOLO)"]
+ rows = [
+ ["Szybkość", "~5 fps", "45-155 fps"],
+ ["Dokładność (mAP)", "wyższa (historycznie)", "dorównuje (YOLOv8)"],
+ ["Małe obiekty", "lepszy", "gorszy (SSD/FPN pomaga)"],
+ ["Architektura", "2 etapy + NMS", "1 etap + NMS"],
+ ["Real-time?", "NIE", "TAK"],
+ ]
+ col_widths = [2.5, 3.5, 3.5]
+ draw_table(
+ ax,
+ headers,
+ rows,
+ 0.2,
+ 3.8,
+ col_widths,
+ row_h=0.65,
+ fontsize=FS,
+ header_fontsize=FS,
+ )
+
+ save_fig(fig, "q24_two_vs_one_stage.png")
+
+
+# ============================================================
+# 13. ROI Pooling illustration
+# ============================================================
+def draw_roi_pooling() -> None:
+ """Draw roi pooling."""
+ fig, axes = plt.subplots(1, 3, figsize=(12, 4))
+ fig.suptitle(
+ "ROI Pooling — dowolny rozmiar → stały rozmiar",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=1.02,
+ )
+
+ # Feature map with ROI
+ ax = axes[0]
+ # Draw feature map grid
+ fm = rng.integers(0, 10, (8, 8))
+ ax.imshow(fm, cmap="gray", vmin=0, vmax=10, alpha=0.3)
+ for i in range(9):
+ ax.axhline(y=i - 0.5, color=LN, lw=0.3)
+ ax.axvline(x=i - 0.5, color=LN, lw=0.3)
+ # ROI rectangle
+ ax.add_patch(
+ mpatches.Rectangle(
+ (1.5, 1.5), 4, 4, facecolor="none", edgecolor=LN, lw=3, linestyle="-"
+ )
+ )
+ ax.text(3.5, 0.8, "ROI", ha="center", fontsize=FS_LABEL, fontweight="bold")
+ ax.set_xlim(-0.5, 7.5)
+ ax.set_ylim(7.5, -0.5)
+ ax.set_title("① Feature map\nz zaznaczonym ROI", fontsize=FS, fontweight="bold")
+ ax.set_xticks([])
+ ax.set_yticks([])
+
+ # ROI divided into grid
+ ax = axes[1]
+ roi_data = np.array(
+ [
+ [1, 3, 2, 1],
+ [0, 5, 1, 6],
+ [0, 4, 1, 0],
+ [7, 2, 9, 1],
+ ]
+ )
+ ax.imshow(roi_data, cmap="gray", vmin=0, vmax=10)
+ for i in range(5):
+ ax.axhline(y=i - 0.5, color=LN, lw=1)
+ ax.axvline(x=i - 0.5, color=LN, lw=1)
+ # Grid lines for 2x2 pooling
+ ax.axhline(y=0.5, color=LN, lw=3, linestyle="--")
+ ax.axvline(x=0.5, color=LN, lw=3, linestyle="--")
+ for i in range(4):
+ for j in range(4):
+ ax.text(
+ j,
+ i,
+ str(roi_data[i, j]),
+ ha="center",
+ va="center",
+ fontsize=10,
+ fontweight="bold",
+ color="white" if roi_data[i, j] > 5 else "black",
+ )
+ ax.set_title("② ROI podzielony\nna siatkę 2x2", fontsize=FS, fontweight="bold")
+ ax.set_xticks([])
+ ax.set_yticks([])
+
+ # Output after pooling
+ ax = axes[2]
+ out = np.array([[5, 6], [7, 9]])
+ ax.imshow(out, cmap="gray", vmin=0, vmax=10)
+ for i in range(3):
+ ax.axhline(y=i - 0.5, color=LN, lw=1.5)
+ ax.axvline(x=i - 0.5, color=LN, lw=1.5)
+ for i in range(2):
+ for j in range(2):
+ ax.text(
+ j,
+ i,
+ str(out[i, j]),
+ ha="center",
+ va="center",
+ fontsize=14,
+ fontweight="bold",
+ color="white" if out[i, j] > 5 else "black",
+ )
+ ax.set_title(
+ "③ Po ROI Pool 2x2\n(max z każdej komórki)", fontsize=FS, fontweight="bold"
+ )
+ ax.set_xticks([])
+ ax.set_yticks([])
+
+ fig.tight_layout()
+ save_fig(fig, "q24_roi_pooling.png")
+
+
+# ============================================================
+# 14. DETR Pipeline
+# ============================================================
+def draw_detr_pipeline() -> None:
+ """Draw detr pipeline."""
+ fig, ax = plt.subplots(figsize=(11, 4.5))
+ ax.set_xlim(-0.5, 11.5)
+ ax.set_ylim(-1, 4.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "DETR — Transformer do detekcji (bez NMS, bez anchorów)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Pipeline
+ draw_box(ax, 0, 1.5, 1.5, 1.5, "Obraz\nwejściowy", fill=GRAY1, fontsize=FS)
+ draw_arrow(ax, 1.6, 2.25, 2.1, 2.25, lw=1.5)
+
+ draw_box(
+ ax,
+ 2.2,
+ 1.5,
+ 1.5,
+ 1.5,
+ "CNN\nBackbone\n(ResNet)",
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 3.8, 2.25, 4.3, 2.25, lw=1.5)
+
+ draw_box(
+ ax,
+ 4.4,
+ 1.5,
+ 1.8,
+ 1.5,
+ "Transformer\nEncoder\n(self-attention)",
+ fill=GRAY2,
+ fontsize=FS,
+ )
+ draw_arrow(ax, 6.3, 2.25, 6.8, 2.25, lw=1.5)
+
+ draw_box(
+ ax,
+ 6.9,
+ 1.5,
+ 1.8,
+ 1.5,
+ "Transformer\nDecoder\n(N=100 queries)",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Output branches
+ draw_arrow(ax, 8.8, 2.5, 9.5, 3.0, lw=1.2)
+ draw_box(ax, 9.6, 2.7, 1.5, 0.7, "klasa₁...klasa₁₀₀", fill=GRAY4, fontsize=FS_SMALL)
+
+ draw_arrow(ax, 8.8, 2.0, 9.5, 1.5, lw=1.2)
+ draw_box(ax, 9.6, 1.2, 1.5, 0.7, "bbox₁...bbox₁₀₀", fill=GRAY4, fontsize=FS_SMALL)
+
+ # Annotations
+ ax.text(
+ 7.8,
+ 0.5,
+ '100 object queries → 5 obiektów + 95x "brak"',
+ ha="center",
+ fontsize=FS,
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ ax.text(
+ 5.5,
+ 0.0,
+ "Hungarian matching (trening): optymalne dopasowanie predykcji do GT",
+ ha="center",
+ fontsize=FS_SMALL,
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ # Big benefit box
+ ax.text(
+ 5.5,
+ 4.0,
+ "BEZ anchorów • BEZ NMS • end-to-end • prosty pipeline",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY2, "edgecolor": GRAY3},
+ )
+
+ save_fig(fig, "q24_detr_pipeline.png")
+
+
+# ============================================================
+# 15. Sliding Window illustration
+# ============================================================
+def draw_sliding_window() -> None:
+ """Draw sliding window."""
+ fig, axes = plt.subplots(1, 3, figsize=(12, 4))
+ fig.suptitle(
+ "Sliding Window — najprostsze podejście do detekcji",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=1.02,
+ )
+
+ # Multi-position
+ ax = axes[0]
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 8, 6, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ # Grid of sliding windows
+ for i in range(4):
+ for j in range(3):
+ ax.add_patch(
+ mpatches.Rectangle(
+ (i * 1.8 + 0.2, j * 1.8 + 0.2),
+ 1.5,
+ 1.5,
+ facecolor="none",
+ edgecolor=LN,
+ lw=0.6,
+ linestyle="--",
+ )
+ )
+ # Highlight current window
+ ax.add_patch(
+ mpatches.Rectangle((2.0, 2.0), 1.5, 1.5, facecolor="none", edgecolor=LN, lw=2.5)
+ )
+ ax.set_xlim(-0.5, 8.5)
+ ax.set_ylim(-0.5, 6.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("① Wiele pozycji\n(krok co 8 px)", fontsize=FS, fontweight="bold")
+
+ # Multi-scale
+ ax = axes[1]
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 6, 5, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ sizes = [(0.8, 0.8), (1.5, 1.5), (2.5, 2.5), (3.5, 3.5)]
+ for i, (w, h) in enumerate(sizes):
+ ax.add_patch(
+ mpatches.Rectangle(
+ (0.3 + i * 0.3, 0.3 + i * 0.3),
+ w,
+ h,
+ facecolor="none",
+ edgecolor=LN,
+ lw=1 + i * 0.3,
+ linestyle=[":", "--", "-.", "-"][i],
+ )
+ )
+ ax.text(3, 0, "4+ skal", ha="center", fontsize=FS_SMALL, fontweight="bold")
+ ax.set_xlim(-0.5, 6.5)
+ ax.set_ylim(-0.5, 5.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "② Wiele skal\n(obiekty mają różne rozmiary)", fontsize=FS, fontweight="bold"
+ )
+
+ # Count
+ ax = axes[2]
+ ax.axis("off")
+ ax.set_xlim(0, 6)
+ ax.set_ylim(0, 5)
+
+ lines = [
+ ("Obraz: 640 x 480 px", 4.5),
+ ("Okno: 64 x 64 px, krok 8 px", 3.8),
+ ("Pozycje: ~72 x 52 = 3 744", 3.1),
+ ("x 5 skal = 18 720 okien", 2.4),
+ ("x klasyfikacja = WOLNE!", 1.7),
+ ("→ ~3h na jeden obraz", 0.8),
+ ]
+ for text, yp in lines:
+ fw = "bold" if "~3h" in text or "WOLNE" in text else "normal"
+ col = GRAY2 if "WOLNE" in text or "~3h" in text else GRAY4
+ ax.text(
+ 3.0,
+ yp,
+ text,
+ ha="center",
+ fontsize=FS,
+ fontweight=fw,
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": col, "edgecolor": GRAY3},
+ )
+
+ ax.set_title(
+ "③ Dlaczego wolne?\n(miliony klasyfikacji)", fontsize=FS, fontweight="bold"
+ )
+
+ fig.tight_layout()
+ save_fig(fig, "q24_sliding_window.png")
+
+
+# ============================================================
+# 16. FPN (Feature Pyramid Network)
+# ============================================================
+def draw_fpn() -> None:
+ """Draw fpn."""
+ fig, ax = plt.subplots(figsize=(9, 5))
+ ax.set_xlim(-0.5, 9.5)
+ ax.set_ylim(-0.5, 5.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "FPN (Feature Pyramid Network) — detekcja obiektów wszystkich rozmiarów",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Bottom-up (backbone)
+ levels = [
+ (0, 0, 2.0, 2.0, "C2\n56x56", "duże\ndetale"),
+ (0, 2.2, 1.5, 1.5, "C3\n28x28", ""),
+ (0, 3.9, 1.0, 1.0, "C4\n14x14", ""),
+ (0, 5.1, 0.6, 0.6, "C5\n7x7", "kontekst"),
+ ]
+
+ for x, y, w, h, label, note in levels:
+ ax.add_patch(
+ mpatches.Rectangle((x, y - h), w, h, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ ax.text(
+ x + w / 2,
+ y - h / 2,
+ label,
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ if note:
+ ax.text(
+ x + w + 0.15,
+ y - h / 2,
+ note,
+ ha="left",
+ va="center",
+ fontsize=5,
+ style="italic",
+ )
+
+ ax.text(
+ 1.0, -0.3, "Bottom-up\n(backbone)", ha="center", fontsize=FS, fontweight="bold"
+ )
+
+ # Top-down + lateral
+ td_levels = [
+ (4.5, 5.1, 0.6, 0.6, "P5"),
+ (4.5, 3.9, 1.0, 1.0, "P4"),
+ (4.5, 2.2, 1.5, 1.5, "P3"),
+ (4.5, 0, 2.0, 2.0, "P2"),
+ ]
+
+ for x, y, w, h, label in td_levels:
+ ax.add_patch(
+ mpatches.Rectangle(
+ (x, y - h + h), w, h, facecolor=GRAY2, edgecolor=LN, lw=1.5
+ )
+ )
+ ax.text(
+ x + w / 2,
+ y - h / 2 + h,
+ label,
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+
+ # Lateral connections
+ for (_, y1, w1, h1, _, _), (x2, y2, _w2, h2, _) in zip(
+ levels, td_levels, strict=False
+ ):
+ draw_arrow(ax, w1 + 0.2, y1 - h1 / 2, x2 - 0.1, y2 + h2 / 2, lw=1, style="->")
+
+ # Top-down arrows
+ for i in range(len(td_levels) - 1):
+ x2, y2, w2, h2, _ = td_levels[i]
+ x3, y3, w3, h3, _ = td_levels[i + 1]
+ draw_arrow(
+ ax,
+ x2 + w2 / 2,
+ y2,
+ x3 + w3 / 2,
+ y3 + h3 + 0.1,
+ lw=1.2,
+ style="->",
+ color=GRAY3,
+ )
+
+ ax.text(
+ 5.5,
+ -0.3,
+ "Top-down + lateral\n(FPN)",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Detection outputs
+ det_labels = ["małe obj.", "średnie", "duże", "b. duże"]
+ for i, (x, y, w, h, _label) in enumerate(td_levels):
+ draw_arrow(ax, x + w + 0.1, y + h / 2, 7.5, y + h / 2, lw=0.8)
+ ax.text(
+ 7.7,
+ y + h / 2,
+ f"detekcja:\n{det_labels[3 - i]}",
+ fontsize=FS_SMALL,
+ va="center",
+ )
+
+ save_fig(fig, "q24_fpn.png")
+
+
+# ============================================================
+# 17. Anchor boxes
+# ============================================================
+def draw_anchor_boxes() -> None:
+ """Draw anchor boxes."""
+ fig, ax = plt.subplots(figsize=(7, 5))
+ ax.set_title(
+ "Anchor boxes — predefiniowane kształty",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ ax.add_patch(mpatches.Rectangle((0, 0), 6, 5, facecolor=GRAY4, edgecolor=LN, lw=1))
+
+ # Center point
+ cx, cy = 3, 2.5
+ ax.plot(cx, cy, "ko", markersize=8, zorder=5)
+ ax.text(cx + 0.15, cy + 0.15, "(x, y)", fontsize=FS, fontweight="bold")
+
+ # 9 anchors: 3 sizes x 3 ratios
+ anchors = [
+ # (w, h, style, label)
+ (0.8, 0.8, "-", "1:1 small"),
+ (1.6, 1.6, "-", "1:1 medium"),
+ (2.4, 2.4, "-", "1:1 large"),
+ (0.6, 1.2, "--", "1:2 small"),
+ (1.2, 2.4, "--", "1:2 medium"),
+ (1.8, 3.6, "--", "1:2 large"),
+ (1.2, 0.6, ":", "2:1 small"),
+ (2.4, 1.2, ":", "2:1 medium"),
+ (3.6, 1.8, ":", "2:1 large"),
+ ]
+
+ for w, h, ls, _label in anchors:
+ rect = mpatches.Rectangle(
+ (cx - w / 2, cy - h / 2),
+ w,
+ h,
+ facecolor="none",
+ edgecolor=LN,
+ lw=1.2,
+ linestyle=ls,
+ )
+ ax.add_patch(rect)
+
+ # Legend-style labels
+ ax.text(
+ 3,
+ -0.5,
+ "9 anchorów = 3 rozmiary x 3 proporcje (1:1, 1:2, 2:1)\n"
+ "Sieć predykuje PRZESUNIĘCIE od najbliższego anchora",
+ ha="center",
+ fontsize=FS,
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ ax.set_xlim(-0.5, 6.5)
+ ax.set_ylim(-1.2, 5.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+
+ save_fig(fig, "q24_anchor_boxes.png")
+
+
+# ============================================================
+# 18. Detection task comparison
+# ============================================================
+def draw_detection_tasks() -> None:
+ """Draw detection tasks."""
+ fig, axes = plt.subplots(1, 3, figsize=(12, 4))
+ fig.suptitle(
+ "Klasyfikacja vs Detekcja vs Segmentacja",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=1.02,
+ )
+
+ # Classification
+ ax = axes[0]
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 4, 4, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ # Simple cat silhouette
+ ax.add_patch(mpatches.Ellipse((2, 2), 2, 1.5, facecolor=GRAY3, edgecolor=LN, lw=1))
+ ax.add_patch(mpatches.Ellipse((2, 3), 1, 0.8, facecolor=GRAY3, edgecolor=LN, lw=1))
+ # Ears
+ ax.plot([1.6, 1.5, 1.8], [3.3, 3.8, 3.4], color=LN, lw=1.5)
+ ax.plot([2.2, 2.5, 2.4], [3.3, 3.8, 3.4], color=LN, lw=1.5)
+ ax.text(
+ 2, -0.4, '→ "KOT" (jedna etykieta)', ha="center", fontsize=FS, fontweight="bold"
+ )
+ ax.set_xlim(-0.5, 4.5)
+ ax.set_ylim(-0.8, 4.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Klasyfikacja\n(co?)", fontsize=FS, fontweight="bold")
+
+ # Detection
+ ax = axes[1]
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 4, 4, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ # Cat
+ ax.add_patch(
+ mpatches.Ellipse((1.2, 2), 1.2, 1, facecolor=GRAY3, edgecolor=LN, lw=1)
+ )
+ ax.add_patch(
+ mpatches.Ellipse((1.2, 2.8), 0.7, 0.5, facecolor=GRAY3, edgecolor=LN, lw=1)
+ )
+ # Dog
+ ax.add_patch(
+ mpatches.Ellipse((3, 1.5), 1.2, 1, facecolor=GRAY2, edgecolor=LN, lw=1)
+ )
+ ax.add_patch(
+ mpatches.Ellipse((3, 2.3), 0.7, 0.5, facecolor=GRAY2, edgecolor=LN, lw=1)
+ )
+ # Bounding boxes
+ ax.add_patch(
+ mpatches.Rectangle((0.3, 1.2), 1.8, 2.2, facecolor="none", edgecolor=LN, lw=2.5)
+ )
+ ax.text(1.2, 3.5, "KOT", ha="center", fontsize=FS_SMALL, fontweight="bold")
+ ax.add_patch(
+ mpatches.Rectangle((2.1, 0.8), 1.7, 2.0, facecolor="none", edgecolor=LN, lw=2.5)
+ )
+ ax.text(3.0, 2.9, "PIES", ha="center", fontsize=FS_SMALL, fontweight="bold")
+ ax.text(
+ 2,
+ -0.4,
+ "→ bbox + klasa (N obiektów)",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.set_xlim(-0.5, 4.5)
+ ax.set_ylim(-0.8, 4.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Detekcja\n(co? + gdzie?)", fontsize=FS, fontweight="bold")
+
+ # Segmentation
+ ax = axes[2]
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 4, 4, facecolor=GRAY4, edgecolor=LN, lw=1.5)
+ )
+ # Cat mask (detailed)
+ theta = np.linspace(0, 2 * np.pi, 30)
+ cat_x = 1.2 + 0.6 * np.cos(theta) + 0.1 * np.sin(3 * theta)
+ cat_y = 2 + 0.5 * np.sin(theta) + 0.1 * np.cos(2 * theta)
+ ax.fill(cat_x, cat_y, facecolor=GRAY3, edgecolor=LN, lw=1.5)
+ # Dog mask
+ dog_x = 3.0 + 0.6 * np.cos(theta) + 0.05 * np.sin(4 * theta)
+ dog_y = 1.5 + 0.5 * np.sin(theta) + 0.08 * np.cos(3 * theta)
+ ax.fill(dog_x, dog_y, facecolor=GRAY2, edgecolor=LN, lw=1.5)
+ ax.text(1.2, 2, "KOT", ha="center", fontsize=FS_SMALL, fontweight="bold")
+ ax.text(3.0, 1.5, "PIES", ha="center", fontsize=FS_SMALL, fontweight="bold")
+ ax.text(
+ 2,
+ -0.4,
+ "→ maska pikseli (per piksel)",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ ax.set_xlim(-0.5, 4.5)
+ ax.set_ylim(-0.8, 4.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Segmentacja\n(dokładna maska)", fontsize=FS, fontweight="bold")
+
+ fig.tight_layout()
+ save_fig(fig, "q24_detection_tasks.png")
+
+
+# ============================================================
+# 19. CNN Architecture overview
+# ============================================================
+def draw_cnn_architecture() -> None:
+ """Draw cnn architecture."""
+ fig, ax = plt.subplots(figsize=(12, 4))
+ ax.set_xlim(-0.5, 12.5)
+ ax.set_ylim(-1, 4.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "CNN — od obrazu do predykcji (architektura)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # Input image
+ draw_box(ax, 0, 0.5, 1.5, 3, "Obraz\n224x224x3", fill=GRAY1, fontsize=FS)
+
+ # Conv1
+ draw_arrow(ax, 1.6, 2.0, 2.1, 2.0, lw=1.2)
+ draw_box(
+ ax, 2.2, 0.8, 1.2, 2.4, "Conv1\n+ReLU\n55x55x96", fill=GRAY4, fontsize=FS_SMALL
+ )
+
+ # Pool1
+ draw_arrow(ax, 3.5, 2.0, 3.9, 2.0, lw=1.2)
+ draw_box(ax, 4.0, 1.0, 1.0, 2.0, "Pool\n27x27\nx96", fill=GRAY2, fontsize=FS_SMALL)
+
+ # Conv2
+ draw_arrow(ax, 5.1, 2.0, 5.5, 2.0, lw=1.2)
+ draw_box(
+ ax,
+ 5.6,
+ 0.8,
+ 1.2,
+ 2.4,
+ "Conv2\n+ReLU\n27x27\nx256",
+ fill=GRAY4,
+ fontsize=FS_SMALL,
+ )
+
+ # Pool2
+ draw_arrow(ax, 6.9, 2.0, 7.3, 2.0, lw=1.2)
+ draw_box(ax, 7.4, 1.2, 0.8, 1.6, "Pool\n13x13\nx256", fill=GRAY2, fontsize=FS_SMALL)
+
+ # More conv...
+ draw_arrow(ax, 8.3, 2.0, 8.7, 2.0, lw=1.2)
+ ax.text(9.0, 2.0, "...", fontsize=14, ha="center", va="center")
+ draw_arrow(ax, 9.3, 2.0, 9.7, 2.0, lw=1.2)
+
+ # FC
+ draw_box(ax, 9.8, 1.2, 1.0, 1.6, "FC\n4096", fill=GRAY3, fontsize=FS)
+
+ draw_arrow(ax, 10.9, 2.0, 11.3, 2.0, lw=1.2)
+
+ # Output
+ draw_box(
+ ax, 11.4, 1.5, 1.0, 1.0, "Softmax\n1000 klas", fill=GRAY1, fontsize=FS_SMALL
+ )
+
+ # Annotations below
+ ax.text(
+ 3.0,
+ 0.0,
+ "rozmiar maleje\n224→55→27→13→6",
+ ha="center",
+ fontsize=FS_SMALL,
+ style="italic",
+ )
+ ax.text(
+ 6.0,
+ 0.0,
+ "kanały rosną\n3→96→256→384",
+ ha="center",
+ fontsize=FS_SMALL,
+ style="italic",
+ )
+ ax.text(
+ 10.0, 0.0, "decyzja\nkońcowa", ha="center", fontsize=FS_SMALL, style="italic"
+ )
+
+ # hierarchy
+ ax.text(
+ 6.0,
+ 4.0,
+ "Hierarchia: krawędzie → rogi → fragmenty → obiekty (K-R-F-O)",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save_fig(fig, "q24_cnn_architecture.png")
+
+
+# ============================================================
+# MAIN
+# ============================================================
+if __name__ == "__main__":
+ print("Generating PYTANIE 24 diagrams...")
+ draw_hog_svm_pipeline()
+ draw_hog_gradient_steps()
+ draw_viola_jones_cascade()
+ draw_haar_features()
+ draw_integral_image()
+ draw_rcnn_evolution()
+ draw_yolo_grid()
+ draw_iou_diagram()
+ draw_nms_steps()
+ draw_detector_from_classifier()
+ draw_svm_hyperplane()
+ draw_two_vs_one_stage()
+ draw_roi_pooling()
+ draw_detr_pipeline()
+ draw_sliding_window()
+ draw_fpn()
+ draw_anchor_boxes()
+ draw_detection_tasks()
+ draw_cnn_architecture()
+ print("\nAll PYTANIE 24 diagrams generated!")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_q31_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_q31_diagrams.py
new file mode 100755
index 0000000..51bdb75
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_q31_diagrams.py
@@ -0,0 +1,1131 @@
+#!/usr/bin/env python3
+"""Generate diagrams for PYTANIE 31: Interaktywne wspomaganie decyzji w warunkach ryzyka.
+
+Diagrams:
+ 1. Payoff matrix + all criteria results comparison (bar chart)
+ 2. Regret matrix construction step-by-step
+ 3. Hurwicz \u03b1 interpolation between maximax and maximin
+ 4. Decision criteria mnemonic map
+ 5. Expected value criterion with probability-weighted bars
+ 6. Decision conditions spectrum (pewność → ryzyko → niepewność)
+
+All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+import numpy as np
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 11
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.2,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=True,
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y), w, h, boxstyle="round,pad=0.05", lw=lw, edgecolor=LN, facecolor=fill
+ )
+ else:
+ rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ )
+
+
+# ============================================================
+# 1. PAYOFF MATRIX + ALL CRITERIA BAR CHART
+# ============================================================
+def draw_criteria_comparison() -> None:
+ """Draw criteria comparison."""
+ fig, axes = plt.subplots(
+ 1, 2, figsize=(8.27, 4.5), gridspec_kw={"width_ratios": [1.2, 1]}
+ )
+
+ # -- Left: Payoff matrix as styled table --
+ ax = axes[0]
+ ax.axis("off")
+ ax.set_xlim(0, 6)
+ ax.set_ylim(0, 6)
+ ax.set_title(
+ "Macierz wypłat (tys. zł)", fontsize=FS_TITLE, fontweight="bold", pad=8
+ )
+
+ # Headers
+ headers_col = ["", "S₁\n(dobra)", "S₂\n(średnia)", "S₃\n(zła)"]
+ rows = [
+ ["A₁ (fabryka)", "200", "50", "-100"],
+ ["A₂ (sklep)", "80", "70", "40"],
+ ["A₃ (obligacje)", "30", "30", "30"],
+ ]
+
+ col_w = [1.8, 1.2, 1.2, 1.2]
+ row_h = 0.7
+ start_y = 4.5
+ start_x = 0.2
+
+ # Draw header row
+ x = start_x
+ for j, h in enumerate(headers_col):
+ fill = GRAY2 if j > 0 else GRAY3
+ rect = mpatches.Rectangle(
+ (x, start_y), col_w[j], row_h, lw=1, edgecolor=LN, facecolor=fill
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + col_w[j] / 2,
+ start_y + row_h / 2,
+ h,
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+ x += col_w[j]
+
+ # Draw data rows
+ for i, row in enumerate(rows):
+ x = start_x
+ y = start_y - (i + 1) * row_h
+ for j, val in enumerate(row):
+ fill = GRAY4 if j == 0 else ("white" if i % 2 == 0 else GRAY1)
+ # Highlight negative
+ if val.startswith("-"):
+ fill = "#D8D8D8"
+ rect = mpatches.Rectangle(
+ (x, y), col_w[j], row_h, lw=1, edgecolor=LN, facecolor=fill
+ )
+ ax.add_patch(rect)
+ fw = "bold" if j == 0 else "normal"
+ ax.text(
+ x + col_w[j] / 2,
+ y + row_h / 2,
+ val,
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight=fw,
+ )
+ x += col_w[j]
+
+ # Probability row for EV
+ x = start_x
+ y = start_y - 4 * row_h
+ probs = ["p (dla E[X]):", "0.5", "0.3", "0.2"]
+ for j, val in enumerate(probs):
+ fill = GRAY5 if j > 0 else GRAY3
+ rect = mpatches.Rectangle(
+ (x, y), col_w[j], row_h * 0.7, lw=1, edgecolor=LN, facecolor=fill
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + col_w[j] / 2,
+ y + row_h * 0.35,
+ val,
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ style="italic",
+ )
+ x += col_w[j]
+
+ # -- Right: Bar chart comparing criteria results --
+ ax2 = axes[1]
+ criteria = [
+ "E[X]",
+ "Laplace",
+ "Maximax",
+ "Maximin",
+ "Hurwicz\n\u03b1=0.6",
+ "Savage",
+ ]
+
+ # Recalculate with probabilities 0.5, 0.3, 0.2
+ # E[X]: A1=200*0.5+50*0.3+(-100)*0.2=100+15-20=95
+ # A2=80*0.5+70*0.3+40*0.2=40+21+8=69
+ # A3=30*0.5+30*0.3+30*0.2=15+9+6=30
+ ev = [95, 69, 30]
+ laplace = [50, 63.3, 30]
+ maximax = [200, 80, 30]
+ maximin = [-100, 40, 30]
+ hurwicz = [80, 64, 30] # alpha=0.6
+ savage_maxregret = [140, 120, 170] # lower = better
+
+ # Which alternative wins for each criterion?
+ winners = [0, 1, 0, 1, 0, 1] # index of winning alternative
+
+ # Display as grouped bar chart - each criterion shows the 3 alternatives
+ x_pos = np.arange(len(criteria))
+ width = 0.22
+ hatches = ["///", "...", "xxx"]
+ labels = ["A₁ (fabryka)", "A₂ (sklep)", "A₃ (obligacje)"]
+
+ all_vals = [
+ [ev[0], laplace[0], maximax[0], maximin[0], hurwicz[0], savage_maxregret[0]],
+ [ev[1], laplace[1], maximax[1], maximin[1], hurwicz[1], savage_maxregret[1]],
+ [ev[2], laplace[2], maximax[2], maximin[2], hurwicz[2], savage_maxregret[2]],
+ ]
+
+ for i in range(3):
+ ax2.bar(
+ x_pos + (i - 1) * width,
+ all_vals[i],
+ width,
+ label=labels[i],
+ color="white",
+ edgecolor=LN,
+ hatch=hatches[i],
+ lw=0.8,
+ )
+
+ # Mark winners with star
+ for c_idx in range(len(criteria)):
+ w = winners[c_idx]
+ val = all_vals[w][c_idx]
+ ax2.text(
+ x_pos[c_idx] + (w - 1) * width,
+ val + 5,
+ "★",
+ ha="center",
+ va="bottom",
+ fontsize=10,
+ fontweight="bold",
+ )
+
+ ax2.set_xticks(x_pos)
+ ax2.set_xticklabels(criteria, fontsize=7)
+ ax2.set_ylabel("Wartość kryterium", fontsize=8)
+ ax2.set_title("Porównanie kryteriów", fontsize=FS_TITLE, fontweight="bold", pad=8)
+ ax2.legend(fontsize=7, loc="upper right")
+ ax2.axhline(y=0, color=LN, lw=0.5, ls="-")
+ ax2.spines["top"].set_visible(False)
+ ax2.spines["right"].set_visible(False)
+ ax2.tick_params(labelsize=7)
+
+ # Note about Savage
+ ax2.text(
+ 5,
+ -30,
+ "(Savage: niżej\n= lepiej)",
+ fontsize=6,
+ ha="center",
+ va="top",
+ style="italic",
+ )
+
+ plt.tight_layout()
+ outpath = str(Path(OUTPUT_DIR) / "q31_criteria_comparison.png")
+ fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" Saved: {outpath}")
+
+
+# ============================================================
+# 2. REGRET MATRIX CONSTRUCTION
+# ============================================================
+def draw_regret_matrix() -> None:
+ """Draw regret matrix."""
+ fig, ax = plt.subplots(1, 1, figsize=(8.27, 5))
+ ax.axis("off")
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 7)
+ ax.set_title(
+ "Kryterium Savage'a — budowa macierzy żalu",
+ fontsize=FS_TITLE + 1,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # --- Step 1: Original payoff matrix (left) ---
+ ax.text(
+ 2.2,
+ 6.3,
+ "Krok 1: Macierz wypłat",
+ fontsize=9,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ )
+
+ col_w = 1.0
+ row_h = 0.55
+ headers = ["", "S₁", "S₂", "S₃"]
+ data = [
+ ["A₁", "200", "50", "-100"],
+ ["A₂", "80", "70", "40"],
+ ["A₃", "30", "30", "30"],
+ ]
+ start_x = 0.3
+ start_y = 5.5
+
+ for j, h in enumerate(headers):
+ w = 0.7 if j == 0 else col_w
+ x = start_x + (0 if j == 0 else 0.7 + (j - 1) * col_w)
+ rect = mpatches.Rectangle(
+ (x, start_y), w, row_h, lw=1, edgecolor=LN, facecolor=GRAY2
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ start_y + row_h / 2,
+ h,
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ for i, row in enumerate(data):
+ y = start_y - (i + 1) * row_h
+ for j, val in enumerate(row):
+ w = 0.7 if j == 0 else col_w
+ x = start_x + (0 if j == 0 else 0.7 + (j - 1) * col_w)
+ fill = GRAY4 if j == 0 else "white"
+ rect = mpatches.Rectangle(
+ (x, y), w, row_h, lw=1, edgecolor=LN, facecolor=fill
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2, y + row_h / 2, val, ha="center", va="center", fontsize=FS
+ )
+
+ # Max per column annotation
+ max_y = start_y - 3 * row_h - 0.1
+ ax.text(
+ start_x + 0.7 + 0.5 * col_w,
+ max_y,
+ "max=200",
+ fontsize=7,
+ ha="center",
+ va="top",
+ fontweight="bold",
+ color="#333",
+ )
+ ax.text(
+ start_x + 0.7 + 1.5 * col_w,
+ max_y,
+ "max=70",
+ fontsize=7,
+ ha="center",
+ va="top",
+ fontweight="bold",
+ color="#333",
+ )
+ ax.text(
+ start_x + 0.7 + 2.5 * col_w,
+ max_y,
+ "max=40",
+ fontsize=7,
+ ha="center",
+ va="top",
+ fontweight="bold",
+ color="#333",
+ )
+
+ # Arrow
+ ax.annotate(
+ "",
+ xy=(5.0, 4.8),
+ xytext=(4.2, 4.8),
+ arrowprops={"arrowstyle": "->", "color": LN, "lw": 2},
+ )
+ ax.text(
+ 4.6,
+ 5.0,
+ "rᵢⱼ = max - aᵢⱼ",
+ fontsize=8,
+ ha="center",
+ va="bottom",
+ fontweight="bold",
+ )
+
+ # --- Step 2: Regret matrix (right) ---
+ ax.text(
+ 7.5,
+ 6.3,
+ "Krok 2: Macierz żalu",
+ fontsize=9,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ )
+
+ regret_data = [
+ ["A₁", "0", "20", "140"],
+ ["A₂", "120", "0", "0"],
+ ["A₃", "170", "40", "10"],
+ ]
+ headers2 = ["", "S₁", "S₂", "S₃", "max rᵢ"]
+ start_x2 = 5.3
+
+ for j, h in enumerate(headers2):
+ w = 0.7 if j == 0 else (0.9 if j < 4 else 1.0)
+ x = start_x2
+ if j == 0:
+ x = start_x2
+ elif j <= 3:
+ x = start_x2 + 0.7 + (j - 1) * 0.9
+ else:
+ x = start_x2 + 0.7 + 3 * 0.9
+ rect = mpatches.Rectangle(
+ (x, start_y),
+ w,
+ row_h,
+ lw=1,
+ edgecolor=LN,
+ facecolor=GRAY2 if j < 4 else GRAY3,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ start_y + row_h / 2,
+ h,
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ max_regrets = [140, 120, 170]
+ for i, row in enumerate(regret_data):
+ y = start_y - (i + 1) * row_h
+ for j, val in enumerate(row):
+ w = 0.7 if j == 0 else 0.9
+ x = start_x2 + (0 if j == 0 else 0.7 + (j - 1) * 0.9)
+ fill = GRAY4 if j == 0 else "white"
+ # Highlight the max regret cell
+ if j > 0 and int(val) == max_regrets[i]:
+ fill = GRAY2
+ rect = mpatches.Rectangle(
+ (x, y), w, row_h, lw=1, edgecolor=LN, facecolor=fill
+ )
+ ax.add_patch(rect)
+ fw = "bold" if (j > 0 and int(val) == max_regrets[i]) else "normal"
+ ax.text(
+ x + w / 2,
+ y + row_h / 2,
+ val,
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight=fw,
+ )
+
+ # Max regret column
+ x = start_x2 + 0.7 + 3 * 0.9
+ w = 1.0
+ fill = "#C8C8C8" if max_regrets[i] == min(max_regrets) else GRAY1
+ rect = mpatches.Rectangle(
+ (x, y),
+ w,
+ row_h,
+ lw=1.5 if max_regrets[i] == min(max_regrets) else 1,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ marker = " ★" if max_regrets[i] == min(max_regrets) else ""
+ ax.text(
+ x + w / 2,
+ y + row_h / 2,
+ f"{max_regrets[i]}{marker}",
+ ha="center",
+ va="center",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Bottom conclusion
+ ax.text(
+ 5.0,
+ 2.8,
+ "Krok 3: Wybierz min z max żalu → A₂ (max żal = 120)",
+ fontsize=10,
+ ha="center",
+ va="center",
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY1,
+ "edgecolor": LN,
+ "lw": 1.5,
+ },
+ )
+
+ # Interpretatation examples
+ ax.text(
+ 5.0,
+ 2.0,
+ "Interpretacja żalu: r₁₃ = 140 oznacza:\n"
+ "„Gdyby nastąpił S₃ (zła koniunktura), a wybrałbym A₁,\n"
+ 'żałowałbym, bo najlepszą opcją byłoby A₂ z wynikiem 40 — traciłbym 140"',
+ fontsize=7.5,
+ ha="center",
+ va="center",
+ style="italic",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ "lw": 0.8,
+ },
+ )
+
+ # Mnemonic
+ ax.text(
+ 5.0,
+ 0.8,
+ 'Mnemonik: Savage = „Żal jak nóż"\nMaksymalny żal to nóż '
+ "— wybierz opcję z NAJMNIEJSZYM nożem",
+ fontsize=8,
+ ha="center",
+ va="center",
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": "white",
+ "edgecolor": LN,
+ "lw": 1,
+ },
+ )
+
+ plt.tight_layout()
+ outpath = str(Path(OUTPUT_DIR) / "q31_regret_matrix.png")
+ fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" Saved: {outpath}")
+
+
+# ============================================================
+# 3. HURWICZ alpha INTERPOLATION
+# ============================================================
+def draw_hurwicz_interpolation() -> None:
+ """Draw hurwicz interpolation."""
+ fig, ax = plt.subplots(1, 1, figsize=(8.27, 4))
+ ax.set_title(
+ "Kryterium Hurwicza — wpływ \u03b1 na wybór alternatywy",
+ fontsize=FS_TITLE + 1,
+ fontweight="bold",
+ pad=10,
+ )
+
+ alphas = np.linspace(0, 1, 200)
+
+ # V(Ai) = alpha * max_i + (1-alpha) * min_i
+ # A1: max=200, min=-100
+ # A2: max=80, min=40
+ # A3: max=30, min=30
+ v1 = alphas * 200 + (1 - alphas) * (-100)
+ v2 = alphas * 80 + (1 - alphas) * 40
+ v3 = alphas * 30 + (1 - alphas) * 30
+
+ ax.plot(alphas, v1, "k-", lw=2, label="A₁ (fabryka): V = 300\u03b1 - 100")
+ ax.plot(alphas, v2, "k--", lw=2, label="A₂ (sklep): V = 40\u03b1 + 40")
+ ax.plot(alphas, v3, "k:", lw=2, label="A₃ (obligacje): V = 30")
+
+ # Find crossover points
+ # A2 = A1: 40alpha + 40 = 300alpha - 100 → 140 = 260alpha → alpha = 140/260 ≈ 0.538
+ alpha_cross_12 = 140 / 260
+ v_cross_12 = 40 * alpha_cross_12 + 40
+
+ # A2 = A3: 40alpha + 40 = 30 → 40alpha = -10 → alpha = -0.25 (never — A2 always > A3)
+ # A1 = A3: 300alpha - 100 = 30 → 300alpha = 130 → alpha = 130/300 ≈ 0.433
+
+ ax.plot(alpha_cross_12, v_cross_12, "ko", markersize=8, zorder=5)
+ ax.annotate(
+ f"\u03b1 ≈ {alpha_cross_12:.2f}\nA₁ = A₂",
+ xy=(alpha_cross_12, v_cross_12),
+ xytext=(alpha_cross_12 + 0.12, v_cross_12 - 30),
+ fontsize=8,
+ fontweight="bold",
+ arrowprops={"arrowstyle": "->", "color": LN, "lw": 1},
+ )
+
+ # Shade winning regions
+ ax.axvspan(0, alpha_cross_12, alpha=0.08, color="black", label="_")
+ ax.axvspan(alpha_cross_12, 1, alpha=0.15, color="black", label="_")
+
+ ax.text(
+ alpha_cross_12 / 2,
+ -60,
+ "A₂ wygrywa\n(pesymistycznie)",
+ fontsize=8,
+ ha="center",
+ va="center",
+ bbox={"boxstyle": "round", "facecolor": "white", "edgecolor": LN},
+ )
+ ax.text(
+ (alpha_cross_12 + 1) / 2,
+ 160,
+ "A₁ wygrywa\n(optymistycznie)",
+ fontsize=8,
+ ha="center",
+ va="center",
+ bbox={"boxstyle": "round", "facecolor": "white", "edgecolor": LN},
+ )
+
+ # Special alpha values
+ ax.axvline(x=0, color=LN, lw=0.5, ls=":")
+ ax.axvline(x=1, color=LN, lw=0.5, ls=":")
+ ax.text(
+ 0,
+ -115,
+ "\u03b1=0\nmaximin",
+ fontsize=7,
+ ha="center",
+ va="top",
+ fontweight="bold",
+ )
+ ax.text(
+ 1,
+ -115,
+ "\u03b1=1\nmaximax",
+ fontsize=7,
+ ha="center",
+ va="top",
+ fontweight="bold",
+ )
+
+ ax.set_xlabel("Współczynnik optymizmu \u03b1", fontsize=9)
+ ax.set_ylabel("V(Aᵢ) = \u03b1·max + (1-\u03b1)·min", fontsize=9)
+ ax.legend(fontsize=8, loc="upper left")
+ ax.spines["top"].set_visible(False)
+ ax.spines["right"].set_visible(False)
+ ax.set_xlim(-0.05, 1.05)
+ ax.axhline(y=0, color=LN, lw=0.3, ls="-")
+ ax.tick_params(labelsize=8)
+
+ plt.tight_layout()
+ outpath = str(Path(OUTPUT_DIR) / "q31_hurwicz_alpha.png")
+ fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" Saved: {outpath}")
+
+
+# ============================================================
+# 4. DECISION CRITERIA MNEMONIC MAP
+# ============================================================
+def draw_criteria_mnemonic() -> None:
+ """Draw criteria mnemonic."""
+ fig, ax = plt.subplots(1, 1, figsize=(8.27, 6))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 8)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Mapa mnemoniczna — 6 kryteriów decyzyjnych",
+ fontsize=FS_TITLE + 2,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Central node
+ draw_box(
+ ax,
+ 3.5,
+ 3.5,
+ 3,
+ 1,
+ "MACIERZ\nWYPŁAT",
+ fill=GRAY2,
+ lw=2,
+ fontsize=11,
+ fontweight="bold",
+ )
+
+ # Criteria boxes around the center
+ criteria = [
+ # (x, y, w, h, title, mnemonic, formula)
+ (
+ 0,
+ 6.5,
+ 3,
+ 1.2,
+ "WARTOŚĆ OCZEKIWANA",
+ '„Mam prawdopodobieństwa"',
+ "E[Aᵢ] = Σ pⱼ·aᵢⱼ",
+ ),
+ (3.5, 6.5, 3, 1.2, "LAPLACE", '„Wszystko po równo"', "V = Σaᵢⱼ / n"),
+ (7, 6.5, 3, 1.2, "MAXIMAX", '„Optymista: max z max"', "max maxⱼ aᵢⱼ"),
+ (0, 0.5, 3, 1.2, "MAXIMIN (Wald)", '„Pesymista: max z min"', "max minⱼ aᵢⱼ"),
+ (
+ 3.5,
+ 0.5,
+ 3,
+ 1.2,
+ "HURWICZ",
+ '„\u03b1 pomiędzy"',
+ "\u03b1·max + (1-\u03b1)·min",
+ ),
+ (7, 0.5, 3, 1.2, "SAVAGE", '„Min max żalu"', "min maxⱼ rᵢⱼ"),
+ ]
+
+ fills = [GRAY3, GRAY1, "white", "white", GRAY1, GRAY3]
+
+ for i, (x, y, w, h, title, mnem, formula) in enumerate(criteria):
+ rect = FancyBboxPatch(
+ (x, y),
+ w,
+ h,
+ boxstyle="round,pad=0.08",
+ lw=1.5,
+ edgecolor=LN,
+ facecolor=fills[i],
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h * 0.78,
+ title,
+ ha="center",
+ va="center",
+ fontsize=8,
+ fontweight="bold",
+ )
+ ax.text(
+ x + w / 2,
+ y + h * 0.45,
+ mnem,
+ ha="center",
+ va="center",
+ fontsize=7,
+ style="italic",
+ )
+ ax.text(
+ x + w / 2,
+ y + h * 0.15,
+ formula,
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ family="monospace",
+ )
+
+ # Arrows from center to each box
+ cx, cy = 5, 4 # center of macierz
+ bx, by = x + w / 2, y + h / 2
+ if by > cy:
+ ax.annotate(
+ "",
+ xy=(bx, y),
+ xytext=(cx, 4.5),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": LN,
+ "lw": 1,
+ "connectionstyle": "arc3,rad=0",
+ },
+ )
+ else:
+ ax.annotate(
+ "",
+ xy=(bx, y + h),
+ xytext=(cx, 3.5),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": LN,
+ "lw": 1,
+ "connectionstyle": "arc3,rad=0",
+ },
+ )
+
+ # Labels on arrows
+ ax.text(
+ 1.2,
+ 5.6,
+ "znane p",
+ fontsize=7,
+ ha="center",
+ va="center",
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "white",
+ "edgecolor": GRAY3,
+ "lw": 0.5,
+ },
+ )
+ ax.text(
+ 5,
+ 5.6,
+ "p = 1/n",
+ fontsize=7,
+ ha="center",
+ va="center",
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "white",
+ "edgecolor": GRAY3,
+ "lw": 0.5,
+ },
+ )
+ ax.text(
+ 8.7,
+ 5.6,
+ "max ↑",
+ fontsize=7,
+ ha="center",
+ va="center",
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "white",
+ "edgecolor": GRAY3,
+ "lw": 0.5,
+ },
+ )
+ ax.text(
+ 1.2,
+ 2.5,
+ "min ↑",
+ fontsize=7,
+ ha="center",
+ va="center",
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "white",
+ "edgecolor": GRAY3,
+ "lw": 0.5,
+ },
+ )
+ ax.text(
+ 5,
+ 2.5,
+ "podaj \u03b1",
+ fontsize=7,
+ ha="center",
+ va="center",
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "white",
+ "edgecolor": GRAY3,
+ "lw": 0.5,
+ },
+ )
+ ax.text(
+ 8.7,
+ 2.5,
+ "macierz\nżalu",
+ fontsize=7,
+ ha="center",
+ va="center",
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "white",
+ "edgecolor": GRAY3,
+ "lw": 0.5,
+ },
+ )
+
+ plt.tight_layout()
+ outpath = str(Path(OUTPUT_DIR) / "q31_criteria_mnemonic.png")
+ fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" Saved: {outpath}")
+
+
+# ============================================================
+# 5. EXPECTED VALUE CRITERION WITH PROBABILITY BARS
+# ============================================================
+def draw_expected_value() -> None:
+ """Draw expected value."""
+ fig, axes = plt.subplots(1, 3, figsize=(8.27, 3.5), sharey=True)
+ fig.suptitle(
+ "Kryterium wartości oczekiwanej E[X] — rozkład wyników per alternatywa",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=1.02,
+ )
+
+ # Probabilities: p1=0.5, p2=0.3, p3=0.2
+ probs = [0.5, 0.3, 0.2]
+ alts = [
+ ("A₁ (fabryka)", [200, 50, -100], 95),
+ ("A₂ (sklep)", [80, 70, 40], 69),
+ ("A₃ (obligacje)", [30, 30, 30], 30),
+ ]
+
+ hatches = ["///", "...", "xxx"]
+
+ for _idx, (ax, (name, vals, ev)) in enumerate(zip(axes, alts, strict=False)):
+ # Bar: height = payoff, width proportional to probability
+ x_positions = [0, 0.6, 1.0]
+ widths = [p * 0.9 for p in probs]
+
+ for i, (v, p, h) in enumerate(zip(vals, probs, hatches, strict=False)):
+ color = "white" if v >= 0 else GRAY2
+ ax.bar(
+ x_positions[i],
+ v,
+ width=widths[i],
+ color=color,
+ edgecolor=LN,
+ hatch=h,
+ lw=0.8,
+ align="edge",
+ )
+ # Value label
+ offset = 8 if v >= 0 else -12
+ ax.text(
+ x_positions[i] + widths[i] / 2,
+ v + offset,
+ f"{v}",
+ ha="center",
+ va="center",
+ fontsize=8,
+ fontweight="bold",
+ )
+ # Probability contribution
+ contrib = v * p
+ ax.text(
+ x_positions[i] + widths[i] / 2,
+ v / 2,
+ f"{v}x{p}\n={contrib:.0f}",
+ ha="center",
+ va="center",
+ fontsize=6,
+ style="italic",
+ )
+
+ # Expected value line
+ ax.axhline(y=ev, color=LN, lw=2, ls="--")
+ ax.text(
+ 1.35,
+ ev,
+ f"E[X]={ev}",
+ fontsize=8,
+ fontweight="bold",
+ va="center",
+ ha="left",
+ bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY1, "edgecolor": LN},
+ )
+
+ ax.set_title(name, fontsize=9, fontweight="bold")
+ ax.set_xticks([0.225, 0.735, 1.09])
+ ax.set_xticklabels(["S₁", "S₂", "S₃"], fontsize=7)
+ ax.axhline(y=0, color=LN, lw=0.5)
+ ax.spines["top"].set_visible(False)
+ ax.spines["right"].set_visible(False)
+ ax.tick_params(labelsize=7)
+
+ # Star on winner
+ if ev == 95:
+ ax.text(
+ 0.7,
+ ev + 20,
+ "★ MAX",
+ fontsize=9,
+ fontweight="bold",
+ ha="center",
+ va="bottom",
+ )
+
+ axes[0].set_ylabel("Wypłata (tys. zł)", fontsize=8)
+
+ plt.tight_layout()
+ outpath = str(Path(OUTPUT_DIR) / "q31_expected_value.png")
+ fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" Saved: {outpath}")
+
+
+# ============================================================
+# 6. DECISION CONDITIONS SPECTRUM
+# ============================================================
+def draw_conditions_spectrum() -> None:
+ """Draw conditions spectrum."""
+ fig, ax = plt.subplots(1, 1, figsize=(8.27, 3.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Warunki decyzyjne — spektrum wiedzy decydenta",
+ fontsize=FS_TITLE + 1,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Three zones
+ zones = [
+ (
+ 0.3,
+ 1.5,
+ 2.8,
+ 2.5,
+ "PEWNOŚĆ",
+ "white",
+ [
+ "Znamy dokładny wynik",
+ "Przykład: lokata 5%",
+ "Metoda: po prostu wybierz",
+ "najlepszy wynik",
+ ],
+ ),
+ (
+ 3.5,
+ 1.5,
+ 2.8,
+ 2.5,
+ "RYZYKO",
+ GRAY1,
+ [
+ "Znamy wyniki I prawdop.",
+ "Przykład: gra w kości",
+ "Metoda: wartość",
+ "oczekiwana E[X]",
+ ],
+ ),
+ (
+ 6.7,
+ 1.5,
+ 2.8,
+ 2.5,
+ "NIEPEWNOŚĆ",
+ GRAY3,
+ [
+ "Znamy wyniki, ale",
+ "NIE znamy prawdop.",
+ "Metody: Laplace, maximax,",
+ "maximin, Hurwicz, Savage",
+ ],
+ ),
+ ]
+
+ for x, y, w, h, title, fill, lines in zones:
+ rect = FancyBboxPatch(
+ (x, y), w, h, boxstyle="round,pad=0.1", lw=2, edgecolor=LN, facecolor=fill
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h - 0.3,
+ title,
+ ha="center",
+ va="center",
+ fontsize=11,
+ fontweight="bold",
+ )
+ for i, line in enumerate(lines):
+ ax.text(
+ x + w / 2,
+ y + h - 0.7 - i * 0.4,
+ line,
+ ha="center",
+ va="center",
+ fontsize=7,
+ )
+
+ # Arrows between zones
+ ax.annotate(
+ "",
+ xy=(3.4, 2.75),
+ xytext=(3.15, 2.75),
+ arrowprops={"arrowstyle": "->", "color": LN, "lw": 2},
+ )
+ ax.annotate(
+ "",
+ xy=(6.6, 2.75),
+ xytext=(6.35, 2.75),
+ arrowprops={"arrowstyle": "->", "color": LN, "lw": 2},
+ )
+
+ # Bottom: knowledge gradient bar
+ gradient_y = 0.5
+ gradient_h = 0.5
+ n_steps = 50
+ for i in range(n_steps):
+ x = 0.3 + i * (9.2 / n_steps)
+ w = 9.2 / n_steps + 0.01
+ gray_val = 1 - (i / n_steps) * 0.7
+ rect = mpatches.Rectangle(
+ (x, gradient_y), w, gradient_h, lw=0, facecolor=str(gray_val)
+ )
+ ax.add_patch(rect)
+
+ rect = mpatches.Rectangle(
+ (0.3, gradient_y), 9.2, gradient_h, lw=1.5, edgecolor=LN, facecolor="none"
+ )
+ ax.add_patch(rect)
+
+ ax.text(0.3, gradient_y - 0.15, "Dużo wiedzy", fontsize=7, ha="left", va="top")
+ ax.text(9.5, gradient_y - 0.15, "Mało wiedzy", fontsize=7, ha="right", va="top")
+ ax.text(
+ 4.95,
+ gradient_y + gradient_h / 2,
+ "POZIOM WIEDZY DECYDENTA",
+ fontsize=8,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ color="white",
+ )
+
+ plt.tight_layout()
+ outpath = str(Path(OUTPUT_DIR) / "q31_conditions_spectrum.png")
+ fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
+ plt.close(fig)
+ print(f" Saved: {outpath}")
+
+
+# ============================================================
+# MAIN
+# ============================================================
+if __name__ == "__main__":
+ print("Generating PYTANIE 31 diagrams...")
+ draw_criteria_comparison()
+ draw_regret_matrix()
+ draw_hurwicz_interpolation()
+ draw_criteria_mnemonic()
+ draw_expected_value()
+ draw_conditions_spectrum()
+ print("Done! All Q31 diagrams saved to:", OUTPUT_DIR)
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_q9_all_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_q9_all_diagrams.py
new file mode 100755
index 0000000..05733fd
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_q9_all_diagrams.py
@@ -0,0 +1,1598 @@
+#!/usr/bin/env python3
+"""Generate ALL diagrams for PYTANIE 9: Procesy i wątki (SOI).
+
+Replaces every ASCII diagram with a monochrome A4-printable PNG (300 DPI).
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+import numpy as np
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 11
+FS_SMALL = 6.5
+FS_LABEL = 9
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.2,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=True,
+ edgecolor=LN,
+ linestyle="-",
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y),
+ w,
+ h,
+ boxstyle="round,pad=0.05",
+ lw=lw,
+ edgecolor=edgecolor,
+ facecolor=fill,
+ linestyle=linestyle,
+ )
+ else:
+ rect = mpatches.Rectangle(
+ (x, y),
+ w,
+ h,
+ lw=lw,
+ edgecolor=edgecolor,
+ facecolor=fill,
+ linestyle=linestyle,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ )
+
+
+def draw_double_arrow(ax, x1, y1, x2, y2, lw=1.2, color=LN) -> None:
+ """Draw double arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": "<->", "color": color, "lw": lw},
+ )
+
+
+def save_fig(fig, name) -> None:
+ """Save fig."""
+ path = str(Path(OUTPUT_DIR) / name)
+ fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG, pad_inches=0.15)
+ plt.close(fig)
+ print(f" Saved: {path}")
+
+
+def draw_table(
+ ax,
+ headers,
+ rows,
+ x0,
+ y0,
+ col_widths,
+ row_h=0.4,
+ header_fill=GRAY2,
+ row_fills=None,
+ fontsize=FS,
+ header_fontsize=None,
+) -> None:
+ """Draw a clean table on axes."""
+ if header_fontsize is None:
+ header_fontsize = fontsize
+ len(headers)
+ len(rows)
+ sum(col_widths)
+
+ # Header
+ cx = x0
+ for j, hdr in enumerate(headers):
+ draw_box(
+ ax,
+ cx,
+ y0,
+ col_widths[j],
+ row_h,
+ hdr,
+ fill=header_fill,
+ fontsize=header_fontsize,
+ fontweight="bold",
+ rounded=False,
+ )
+ cx += col_widths[j]
+
+ # Rows
+ for i, row in enumerate(rows):
+ cy = y0 - (i + 1) * row_h
+ cx = x0
+ fill = GRAY4 if (i % 2 == 0) else "white"
+ if row_fills and i < len(row_fills):
+ fill = row_fills[i]
+ for j, cell in enumerate(row):
+ fw = "bold" if j == 0 else "normal"
+ draw_box(
+ ax,
+ cx,
+ cy,
+ col_widths[j],
+ row_h,
+ cell,
+ fill=fill,
+ fontsize=fontsize,
+ fontweight=fw,
+ rounded=False,
+ )
+ cx += col_widths[j]
+
+
+# ============================================================
+# 1. Process vs Thread comparison table
+# ============================================================
+def gen_process_vs_thread() -> None:
+ """Gen process vs thread."""
+ fig, ax = plt.subplots(figsize=(7.5, 4.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(-4.5, 1.5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Proces vs Wątek — porównanie", fontsize=FS_TITLE, fontweight="bold", pad=10
+ )
+
+ headers = ["Cecha", "Proces", "Wątek"]
+ col_w = [2.5, 3.5, 3.5]
+ rows = [
+ ["Pamięć", "Własna, izolowana", "Współdzielona (heap)"],
+ ["Tworzenie", "~1-10 ms", "~10-100 μs (100x szybciej)"],
+ ["Przełączanie", "~1-5 μs (TLB flush)", "~0.1-0.5 μs (10x)"],
+ ["Komunikacja", "IPC (pipe, socket, shm)", "Bezpośrednia (wspólna pam.)"],
+ ["Izolacja", "Pełna — awaria izolowana", "Brak — może zabić proces"],
+ ["Zastosowanie", "Bezpieczeństwo, izolacja", "Wydajność, współdzielenie"],
+ ]
+ draw_table(
+ ax,
+ headers,
+ rows,
+ x0=0.25,
+ y0=0.8,
+ col_widths=col_w,
+ row_h=0.55,
+ fontsize=7.5,
+ header_fontsize=FS_LABEL,
+ )
+
+ # Analogy at bottom
+ ax.text(
+ 5.0,
+ -4.2,
+ "Analogia: Proces = mieszkanie (własny adres) "
+ "Wątek = pokój w mieszkaniu (wspólna kuchnia = heap)",
+ ha="center",
+ fontsize=FS,
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save_fig(fig, "q9_process_vs_thread.png")
+
+
+# ============================================================
+# 2. Memory segments layout
+# ============================================================
+def gen_memory_layout() -> None:
+ """Gen memory layout."""
+ fig, ax = plt.subplots(figsize=(6, 5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 8)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Segmenty pamięci procesu", fontsize=FS_TITLE, fontweight="bold", pad=10
+ )
+
+ segments = [
+ ("STACK ↓", "zmienne lokalne, adresy\npowrotu (każdy wątek WŁASNY)", GRAY1),
+ ("...", "(wolna przestrzeń)", "white"),
+ ("HEAP ↑", "malloc/new — dynamiczna\nalokacja (współdzielony)", GRAY4),
+ ("BSS", "zmienne globalne\nniezainicjalizowane (zerowane)", GRAY2),
+ ("DATA", "zmienne globalne\nzainicjalizowane", GRAY3),
+ ("TEXT", "kod maszynowy\n(read-only, współdzielony)", GRAY5),
+ ]
+ bx, bw = 2.0, 2.5
+ seg_h = 0.9
+ gap = 0.05
+ top_y = 7.0
+
+ for i, (name, desc, color) in enumerate(segments):
+ y = top_y - i * (seg_h + gap)
+ draw_box(
+ ax,
+ bx,
+ y,
+ bw,
+ seg_h,
+ name,
+ fill=color,
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ rounded=False,
+ )
+ ax.text(bx + bw + 0.3, y + seg_h / 2, desc, fontsize=7.5, va="center")
+
+ # Address labels
+ ax.text(
+ bx - 0.2,
+ top_y + seg_h / 2,
+ "wysoki\nadres",
+ fontsize=FS_SMALL,
+ va="center",
+ ha="right",
+ style="italic",
+ )
+ bottom_y = top_y - 5 * (seg_h + gap)
+ ax.text(
+ bx - 0.2,
+ bottom_y + seg_h / 2,
+ "niski\nadres",
+ fontsize=FS_SMALL,
+ va="center",
+ ha="right",
+ style="italic",
+ )
+
+ # Arrows for growth
+ ax.annotate(
+ "",
+ xy=(bx - 0.5, top_y - 0.1),
+ xytext=(bx - 0.5, top_y + seg_h + 0.1),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+ ax.text(bx - 0.9, top_y + 0.4, "rośnie\nw dół", fontsize=FS_SMALL, ha="center")
+
+ heap_y = top_y - 2 * (seg_h + gap)
+ ax.annotate(
+ "",
+ xy=(bx - 0.5, heap_y + seg_h + 0.1),
+ xytext=(bx - 0.5, heap_y - 0.1),
+ arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
+ )
+ ax.text(bx - 0.9, heap_y + 0.5, "rośnie\nw górę", fontsize=FS_SMALL, ha="center")
+
+ save_fig(fig, "q9_memory_layout.png")
+
+
+# ============================================================
+# 3. Process states diagram
+# ============================================================
+def gen_process_states() -> None:
+ """Gen process states."""
+ fig, ax = plt.subplots(figsize=(7, 3.5))
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Stany procesu — diagram przejść", fontsize=FS_TITLE, fontweight="bold", pad=10
+ )
+
+ states = {
+ "NEW": (1.0, 2.5),
+ "READY": (3.5, 2.5),
+ "RUNNING": (6.5, 2.5),
+ "BLOCKED": (6.5, 0.5),
+ "TERMINATED": (10.0, 2.5),
+ }
+ fills = {
+ "NEW": GRAY4,
+ "READY": GRAY1,
+ "RUNNING": GRAY3,
+ "BLOCKED": GRAY2,
+ "TERMINATED": GRAY5,
+ }
+ bw, bh = 1.8, 0.9
+ for name, (x, y) in states.items():
+ draw_box(
+ ax,
+ x,
+ y,
+ bw,
+ bh,
+ name,
+ fill=fills[name],
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ )
+
+ # Transitions
+ transitions = [
+ ("NEW", "READY", "admit"),
+ ("READY", "RUNNING", "dispatch\n(scheduler)"),
+ ("RUNNING", "TERMINATED", "exit"),
+ ("RUNNING", "BLOCKED", "I/O wait"),
+ ]
+ for src, dst, label in transitions:
+ sx, sy = states[src]
+ dx, dy = states[dst]
+ if sy == dy: # horizontal
+ draw_arrow(ax, sx + bw, sy + bh / 2, dx, dy + bh / 2, lw=1.5)
+ mx = (sx + bw + dx) / 2
+ ax.text(
+ mx,
+ sy + bh / 2 + 0.25,
+ label,
+ fontsize=FS_SMALL,
+ ha="center",
+ va="bottom",
+ )
+ else: # vertical
+ draw_arrow(ax, sx + bw / 2, sy, dx + bw / 2, dy + bh, lw=1.5)
+ ax.text(
+ sx + bw + 0.2,
+ (sy + dy + bh) / 2,
+ label,
+ fontsize=FS_SMALL,
+ ha="left",
+ va="center",
+ )
+
+ # BLOCKED → READY
+ bx, by = states["BLOCKED"]
+ rx, ry = states["READY"]
+ ax.annotate(
+ "",
+ xy=(rx + bw / 2, ry),
+ xytext=(bx - 0.3, by + bh / 2),
+ arrowprops={
+ "arrowstyle": "->",
+ "lw": 1.5,
+ "color": LN,
+ "connectionstyle": "arc3,rad=0.3",
+ },
+ )
+ ax.text(3.5, 0.7, "I/O done", fontsize=FS_SMALL, ha="center")
+
+ # RUNNING → READY (preemption)
+ rux, ruy = states["RUNNING"]
+ draw_arrow(ax, rux, ruy + bh, rx + bw, ry + bh, lw=1.2)
+ ax.text(5.0, 3.7, "preempt /\ntimeout", fontsize=FS_SMALL, ha="center")
+
+ save_fig(fig, "q9_process_states.png")
+
+
+# ============================================================
+# 4. Thread structure within process
+# ============================================================
+def gen_thread_structure() -> None:
+ """Gen thread structure."""
+ fig, ax = plt.subplots(figsize=(8, 4.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 6)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Wątki wewnątrz procesu (PID=42)", fontsize=FS_TITLE, fontweight="bold", pad=10
+ )
+
+ # Shared memory region
+ draw_box(ax, 0.5, 3.5, 9.0, 1.8, "", fill=GRAY1, rounded=False, lw=2)
+ ax.text(5.0, 5.0, "WSPÓŁDZIELONE", fontsize=FS, fontweight="bold", ha="center")
+
+ labels_shared = ["TEXT", "DATA", "BSS", "HEAP", "pliki", "PID"]
+ for i, lab in enumerate(labels_shared):
+ x = 1.0 + i * 1.4
+ draw_box(
+ ax,
+ x,
+ 3.8,
+ 1.1,
+ 0.6,
+ lab,
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ rounded=False,
+ )
+ ax.text(x + 0.55, 4.6, lab, fontsize=FS_SMALL, ha="center", color="#555555")
+
+ # Per-thread regions
+ draw_box(
+ ax, 0.5, 0.5, 9.0, 2.7, "", fill="white", rounded=False, lw=2, linestyle="--"
+ )
+ ax.text(
+ 5.0, 2.95, "PRYWATNE (każdy wątek)", fontsize=FS, fontweight="bold", ha="center"
+ )
+
+ for i in range(3):
+ x = 1.0 + i * 3.0
+ tid = i + 1
+ draw_box(ax, x, 0.7, 2.3, 2.0, "", fill=GRAY4, rounded=False)
+ ax.text(
+ x + 1.15,
+ 2.4,
+ f"Wątek {tid}",
+ fontsize=FS_LABEL,
+ fontweight="bold",
+ ha="center",
+ )
+ items = [f"stos_{tid}", f"rejestry_{tid}", f"PC_{tid}", f"TID={40 + tid}"]
+ for j, item in enumerate(items):
+ ax.text(
+ x + 1.15,
+ 2.0 - j * 0.35,
+ item,
+ fontsize=FS_SMALL,
+ ha="center",
+ family="monospace",
+ )
+
+ save_fig(fig, "q9_thread_structure.png")
+
+
+# ============================================================
+# 5. PCB structure
+# ============================================================
+def gen_pcb_structure() -> None:
+ """Gen pcb structure."""
+ fig, ax = plt.subplots(figsize=(5, 3.5))
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 5.5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "PCB (Process Control Block)", fontsize=FS_TITLE, fontweight="bold", pad=10
+ )
+
+ fields = [
+ ("PID", "42"),
+ ("Stan", "READY / RUNNING / BLOCKED"),
+ ("Rejestry CPU", "EAX, EBX, ESP, EIP ..."),
+ ("Tablice stron", "mapowanie wirtualne → fizyczne"),
+ ("Otwarte pliki", "fd[0], fd[1], fd[2] ..."),
+ ("Priorytety", "nice value, scheduling class"),
+ ("Statystyki", "CPU time, I/O count"),
+ ]
+
+ top_y = 4.8
+ for i, (field, value) in enumerate(fields):
+ y = top_y - i * 0.55
+ draw_box(
+ ax,
+ 0.5,
+ y,
+ 2.2,
+ 0.45,
+ field,
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ rounded=False,
+ )
+ draw_box(ax, 2.7, y, 4.5, 0.45, value, fill=GRAY4, fontsize=FS, rounded=False)
+
+ ax.text(
+ 4.0,
+ 0.3,
+ "Context switch = zapisz PCB starego → wczytaj PCB nowego",
+ fontsize=FS_SMALL,
+ ha="center",
+ style="italic",
+ )
+
+ save_fig(fig, "q9_pcb_structure.png")
+
+
+# ============================================================
+# 6. Speed comparison
+# ============================================================
+def gen_speed_comparison() -> None:
+ """Gen speed comparison."""
+ fig, axes = plt.subplots(1, 2, figsize=(9, 3.5))
+ fig.suptitle(
+ "Szybkość — procesy vs wątki (benchmarki Linux)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ )
+
+ # Left: creation
+ ax = axes[0]
+ ops = ["fork()\n(nowy proces)", "pthread_create()\n(nowy wątek)"]
+ times = [3.0, 0.05] # ms
+ colors = [GRAY3, GRAY1]
+ bars = ax.barh(ops, times, color=colors, edgecolor=LN, height=0.5, linewidth=1.2)
+ ax.set_xlabel("Czas [ms]", fontsize=FS)
+ ax.set_title("Tworzenie", fontsize=FS_LABEL, fontweight="bold")
+ ax.set_xlim(0, 4.5)
+ for bar, t in zip(bars, times, strict=False):
+ ax.text(
+ bar.get_width() + 0.1,
+ bar.get_y() + bar.get_height() / 2,
+ f"{t} ms",
+ va="center",
+ fontsize=FS,
+ )
+ ax.text(
+ 2.5,
+ -0.6,
+ "~100x szybciej",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ transform=ax.get_xaxis_transform(),
+ )
+ ax.tick_params(labelsize=FS)
+
+ # Right: context switch
+ ax = axes[1]
+ ops2 = ["Proces→Proces\n(TLB flush)", "Wątek→Wątek\n(TLB warm)"]
+ times2 = [3000, 300] # ns
+ bars2 = ax.barh(ops2, times2, color=colors, edgecolor=LN, height=0.5, linewidth=1.2)
+ ax.set_xlabel("Czas [ns]", fontsize=FS)
+ ax.set_title("Przełączanie kontekstu", fontsize=FS_LABEL, fontweight="bold")
+ ax.set_xlim(0, 4500)
+ for bar, t in zip(bars2, times2, strict=False):
+ ax.text(
+ bar.get_width() + 50,
+ bar.get_y() + bar.get_height() / 2,
+ f"{t} ns",
+ va="center",
+ fontsize=FS,
+ )
+ ax.text(
+ 2500,
+ -0.6,
+ "~10x szybciej",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ transform=ax.get_xaxis_transform(),
+ )
+ ax.tick_params(labelsize=FS)
+
+ fig.tight_layout(rect=[0, 0.05, 1, 0.92])
+ save_fig(fig, "q9_speed_comparison.png")
+
+
+# ============================================================
+# 7. Scenario table (when to use process vs thread)
+# ============================================================
+def gen_scenario_table() -> None:
+ """Gen scenario table."""
+ fig, ax = plt.subplots(figsize=(8.5, 4.5))
+ ax.set_xlim(0, 11)
+ ax.set_ylim(-5.5, 1)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Kiedy proces, kiedy wątek? — typowe scenariusze",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ headers = ["Scenariusz", "Wybór", "Dlaczego?"]
+ col_w = [3.5, 2.5, 4.5]
+ rows = [
+ ["Serwer WWW (Apache)", "Proces", "izolacja klientów"],
+ ["Serwer WWW (nginx)", "Wątek / async", "szybkość, cooperacja"],
+ ["Przeglądarka (karty)", "Proces", "crash isolation"],
+ ["Przeglądarka (JS+render)", "Wątek", "współdzielony DOM"],
+ ["Gra (fizyka+rendering)", "Wątek", "współdzielony świat gry"],
+ ["Kompilacja (make -j8)", "Proces", "izolacja, prostota"],
+ ["Baza danych (zapytania)", "Wątek", "współdzielony cache"],
+ ["Microservices", "Proces (kontener)", "izolacja, deployment"],
+ ]
+ draw_table(
+ ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.5, fontsize=7
+ )
+
+ save_fig(fig, "q9_scenario_table.png")
+
+
+# ============================================================
+# 8. IPC details: pipe, shared memory, socket (3-panel)
+# ============================================================
+def gen_ipc_details() -> None:
+ """Gen ipc details."""
+ fig, axes = plt.subplots(1, 3, figsize=(11, 3.5))
+ fig.suptitle("Mechanizmy IPC — szczegóły", fontsize=FS_TITLE, fontweight="bold")
+
+ # Panel 1: Pipe
+ ax = axes[0]
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("Pipe (potok)", fontsize=FS_LABEL, fontweight="bold")
+
+ draw_box(ax, 0.2, 2.0, 1.8, 1.2, "Proces A\n(ls)\nstdout", fill=GRAY1, fontsize=FS)
+ draw_box(
+ ax,
+ 3.0,
+ 2.0,
+ 1.8,
+ 1.2,
+ "Bufor\njądra\n(4 KB)",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_box(ax, 5.8, 2.0, 1.8, 1.2, "Proces B\n(grep)\nstdin", fill=GRAY1, fontsize=FS)
+ draw_arrow(ax, 2.0, 2.6, 3.0, 2.6, lw=1.5)
+ ax.text(2.5, 3.0, "write()\nfd[1]", fontsize=FS_SMALL, ha="center")
+ draw_arrow(ax, 4.8, 2.6, 5.8, 2.6, lw=1.5)
+ ax.text(5.3, 3.0, "read()\nfd[0]", fontsize=FS_SMALL, ha="center")
+ ax.text(
+ 4.0,
+ 0.8,
+ "Jednokierunkowy\nBufor pełny → write() blokuje",
+ fontsize=FS_SMALL,
+ ha="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # Panel 2: Shared Memory
+ ax = axes[1]
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("Shared Memory", fontsize=FS_LABEL, fontweight="bold")
+
+ draw_box(ax, 0.3, 3.0, 2.2, 1.2, "Proces A\nstrona 7", fill=GRAY1, fontsize=FS)
+ draw_box(ax, 5.5, 3.0, 2.2, 1.2, "Proces B\nstrona 3", fill=GRAY1, fontsize=FS)
+ draw_box(
+ ax,
+ 2.8,
+ 1.0,
+ 2.4,
+ 1.2,
+ "RAM\nramka 42",
+ fill=GRAY3,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, 2.0, 3.0, 3.5, 2.2, lw=1.5)
+ draw_arrow(ax, 6.0, 3.0, 4.5, 2.2, lw=1.5)
+ ax.text(
+ 4.0,
+ 0.3,
+ "Zero kopiowania!\nA pisze → B widzi od razu\nWymaga synchronizacji (semafor)",
+ fontsize=FS_SMALL,
+ ha="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # Panel 3: Socket
+ ax = axes[2]
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("Socket", fontsize=FS_LABEL, fontweight="bold")
+
+ # Network socket
+ draw_box(
+ ax, 0.3, 3.2, 1.8, 0.9, "Klient", fill=GRAY1, fontsize=FS, fontweight="bold"
+ )
+ draw_box(
+ ax, 5.5, 3.2, 1.8, 0.9, "Serwer", fill=GRAY1, fontsize=FS, fontweight="bold"
+ )
+ draw_double_arrow(ax, 2.1, 3.65, 5.5, 3.65, lw=1.5)
+ ax.text(3.8, 4.3, "TCP/IP (sieciowy)", fontsize=FS, ha="center", fontweight="bold")
+
+ # Unix socket
+ draw_box(
+ ax, 0.3, 1.3, 1.8, 0.9, "Proces A", fill=GRAY4, fontsize=FS, fontweight="bold"
+ )
+ draw_box(
+ ax, 5.5, 1.3, 1.8, 0.9, "Proces B", fill=GRAY4, fontsize=FS, fontweight="bold"
+ )
+ draw_double_arrow(ax, 2.1, 1.75, 5.5, 1.75, lw=1.5)
+ ax.text(
+ 3.8,
+ 2.4,
+ "Unix domain socket\n(/tmp/app.sock)",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ )
+
+ ax.text(
+ 3.8,
+ 0.5,
+ "Dwukierunkowy\nNajbardziej uniwersalny IPC",
+ fontsize=FS_SMALL,
+ ha="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.9])
+ save_fig(fig, "q9_ipc_details.png")
+
+
+# ============================================================
+# 9. IPC comparison table
+# ============================================================
+def gen_ipc_table() -> None:
+ """Gen ipc table."""
+ fig, ax = plt.subplots(figsize=(8.5, 3.5))
+ ax.set_xlim(0, 11)
+ ax.set_ylim(-4.5, 1)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Porównanie mechanizmów IPC", fontsize=FS_TITLE, fontweight="bold", pad=10
+ )
+
+ headers = ["Mechanizm", "Kierunek", "Szybkość", "Zastosowanie"]
+ col_w = [2.5, 2.0, 2.5, 3.5]
+ rows = [
+ ["Pipe", "jednokierunkowy", "średnia", "ls | grep"],
+ ["Named Pipe", "jednokierunkowy", "średnia", "demon → klient"],
+ ["Shared Memory", "dwukierunkowy", "NAJSZYBSZA", "video, bazy danych"],
+ ["Message Queue", "dwukierunkowy", "średnia", "wieloproducentowe"],
+ ["Socket", "dwukierunkowy", "wolna (sieć)", "klient-serwer"],
+ ["Signal", "jednokierunkowy", "natychmiastowa", "powiadomienia (nr)"],
+ ]
+ draw_table(
+ ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.5, fontsize=7.5
+ )
+
+ save_fig(fig, "q9_ipc_table.png")
+
+
+# ============================================================
+# 10. Race condition (simple x + bank timeline)
+# ============================================================
+def gen_race_condition() -> None:
+ """Gen race condition."""
+ fig, axes = plt.subplots(1, 2, figsize=(11, 5))
+ fig.suptitle(
+ "Wyścig (Race Condition) — przykłady", fontsize=FS_TITLE, fontweight="bold"
+ )
+
+ # Panel 1: simple x increment
+ ax = axes[0]
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("Prosty wyścig: x = x + 1", fontsize=FS_LABEL, fontweight="bold")
+
+ # Timeline
+ steps_a = ["czytaj x (=0)", "dodaj 1", "zapisz x (=1)"]
+ steps_b = ["czytaj x (=0)", "dodaj 1", "zapisz x (=1)"]
+ ax.text(2.0, 6.3, "Wątek A", fontsize=FS_LABEL, ha="center", fontweight="bold")
+ ax.text(6.0, 6.3, "Wątek B", fontsize=FS_LABEL, ha="center", fontweight="bold")
+ ax.plot([2, 2], [0.8, 6.0], color=LN, lw=1)
+ ax.plot([6, 6], [0.8, 6.0], color=LN, lw=1)
+
+ for i, (sa, sb) in enumerate(zip(steps_a, steps_b, strict=False)):
+ y = 5.3 - i * 1.2
+ draw_box(ax, 0.5, y, 3.0, 0.6, sa, fill=GRAY4, fontsize=FS)
+ draw_box(ax, 4.5, y - 0.3, 3.0, 0.6, sb, fill=GRAY1, fontsize=FS)
+
+ ax.text(
+ 4.0,
+ 0.4,
+ "Wynik: x = 1 (powinno 2!)",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ color="#C62828",
+ bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
+ )
+
+ # Panel 2: bank account
+ ax = axes[1]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("Konto bankowe: saldo = 1000 zł", fontsize=FS_LABEL, fontweight="bold")
+
+ ax.text(2.5, 6.3, "Wątek A (+500)", fontsize=FS, ha="center", fontweight="bold")
+ ax.text(7.5, 6.3, "Wątek B (-200)", fontsize=FS, ha="center", fontweight="bold")
+ ax.plot([2.5, 2.5], [0.8, 6.0], color=LN, lw=1)
+ ax.plot([7.5, 7.5], [0.8, 6.0], color=LN, lw=1)
+
+ events = [
+ ("t1", "czytaj → 1000", "", 5.3),
+ ("t2", "", "czytaj → 1000", 4.6),
+ ("t3", "1000+500=1500", "", 3.9),
+ ("t4", "", "1000-200=800", 3.2),
+ ("t5", "zapisz 1500", "", 2.5),
+ ("t6", "", "zapisz 800 ✗", 1.8),
+ ]
+ for t, a, b, y in events:
+ ax.text(0.3, y + 0.15, t, fontsize=FS_SMALL, fontweight="bold", va="center")
+ if a:
+ draw_box(ax, 1.0, y, 3.0, 0.45, a, fill=GRAY4, fontsize=FS_SMALL)
+ if b:
+ fill = "#F8D7DA" if "✗" in b else GRAY1
+ draw_box(ax, 6.0, y, 3.0, 0.45, b, fill=fill, fontsize=FS_SMALL)
+
+ ax.text(
+ 5.0,
+ 0.4,
+ "Wynik: 800 zł (powinno 1300!)",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ color="#C62828",
+ bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.9])
+ save_fig(fig, "q9_race_condition.png")
+
+
+# ============================================================
+# 11. Deadlock scenario + cycle
+# ============================================================
+def gen_deadlock_scenario() -> None:
+ """Gen deadlock scenario."""
+ fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
+ fig.suptitle("Zakleszczenie (Deadlock)", fontsize=FS_TITLE, fontweight="bold")
+
+ # Panel 1: timeline
+ ax = axes[0]
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 6)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("Scenariusz z 2 mutexami", fontsize=FS_LABEL, fontweight="bold")
+
+ ax.text(2.5, 5.3, "Wątek A", fontsize=FS_LABEL, ha="center", fontweight="bold")
+ ax.text(6.0, 5.3, "Wątek B", fontsize=FS_LABEL, ha="center", fontweight="bold")
+
+ steps = [
+ ("lock(mutex1) OK", "", "trzyma", False, 4.5),
+ ("", "lock(mutex2) OK", "trzyma", False, 3.7),
+ ("lock(mutex2) ...WAIT", "", "CZEKA!", True, 2.9),
+ ("", "lock(mutex1) ...WAIT", "CZEKA!", True, 2.1),
+ ]
+ for a_text, b_text, _note, is_wait, y in steps:
+ if a_text:
+ fill = "#F8D7DA" if is_wait else GRAY4
+ draw_box(ax, 0.5, y, 3.3, 0.55, a_text, fill=fill, fontsize=FS_SMALL)
+ if b_text:
+ fill = "#F8D7DA" if is_wait else GRAY4
+ draw_box(ax, 4.3, y, 3.3, 0.55, b_text, fill=fill, fontsize=FS_SMALL)
+
+ ax.text(
+ 4.0,
+ 1.2,
+ "DEADLOCK!\nŻaden nie odpuści",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ color="#C62828",
+ bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
+ )
+
+ # Panel 2: cycle diagram
+ ax = axes[1]
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 6)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("Cykl oczekiwania", fontsize=FS_LABEL, fontweight="bold")
+
+ # Thread boxes
+ draw_box(
+ ax,
+ 0.5,
+ 3.5,
+ 2.2,
+ 1.2,
+ "Wątek A\ntrzyma Mutex 1",
+ fill=GRAY1,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_box(
+ ax,
+ 5.3,
+ 3.5,
+ 2.2,
+ 1.2,
+ "Wątek B\ntrzyma Mutex 2",
+ fill=GRAY1,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Mutex boxes
+ draw_box(
+ ax, 0.5, 1.0, 2.2, 1.0, "Mutex 1", fill=GRAY3, fontsize=FS, fontweight="bold"
+ )
+ draw_box(
+ ax, 5.3, 1.0, 2.2, 1.0, "Mutex 2", fill=GRAY3, fontsize=FS, fontweight="bold"
+ )
+
+ # holds arrows (down)
+ draw_arrow(ax, 1.6, 3.5, 1.6, 2.0, lw=2)
+ ax.text(0.9, 2.7, "trzyma", fontsize=FS_SMALL, rotation=90, va="center")
+ draw_arrow(ax, 6.4, 3.5, 6.4, 2.0, lw=2)
+ ax.text(7.0, 2.7, "trzyma", fontsize=FS_SMALL, rotation=90, va="center")
+
+ # waits-for arrows (across, red)
+ draw_arrow(ax, 2.7, 4.3, 5.3, 4.3, lw=2.5, color="#C62828")
+ ax.text(
+ 4.0,
+ 4.7,
+ "czeka na Mutex 2",
+ fontsize=FS_SMALL,
+ ha="center",
+ fontweight="bold",
+ color="#C62828",
+ )
+ draw_arrow(ax, 5.3, 3.7, 2.7, 3.7, lw=2.5, color="#C62828")
+ ax.text(
+ 4.0,
+ 3.1,
+ "czeka na Mutex 1",
+ fontsize=FS_SMALL,
+ ha="center",
+ fontweight="bold",
+ color="#C62828",
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.9])
+ save_fig(fig, "q9_deadlock_scenario.png")
+
+
+# ============================================================
+# 12. Coffman conditions + prevention strategies
+# ============================================================
+def gen_coffman_strategies() -> None:
+ """Gen coffman strategies."""
+ fig, ax = plt.subplots(figsize=(9, 4))
+ ax.set_xlim(0, 11.5)
+ ax.set_ylim(-3.5, 1)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Warunki Coffmana — zapobieganie deadlockowi",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ headers = ["Warunek", "Opis", "Jak złamać", "Przykład"]
+ col_w = [2.5, 2.5, 3.0, 3.0]
+ rows = [
+ [
+ "1. Mutual Exclusion",
+ "zasób wyłączny",
+ "współdzielony zasób",
+ "Read-write lock",
+ ],
+ [
+ "2. Hold and Wait",
+ "trzymaj + czekaj",
+ "bierz WSZYSTKIE naraz",
+ "lock(m1,m2) atomowo",
+ ],
+ [
+ "3. No Preemption",
+ "nie zabierzesz siłą",
+ "timeout / trylock",
+ "pthread_mutex_trylock()",
+ ],
+ [
+ "4. Circular Wait",
+ "cykliczne oczekiw.",
+ "porządek liniowy",
+ "zawsze m1 przed m2",
+ ],
+ ]
+ draw_table(
+ ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.6, fontsize=7
+ )
+
+ ax.text(
+ 5.75,
+ -3.1,
+ "▸ Najczęstsza strategia: PORZĄDEK LINIOWY — "
+ "numeruj mutexy, zawsze blokuj rosnąco",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save_fig(fig, "q9_coffman_strategies.png")
+
+
+# ============================================================
+# 13. Starvation + Priority Inversion (2-panel)
+# ============================================================
+def gen_starvation_priority() -> None:
+ """Gen starvation priority."""
+ fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
+ fig.suptitle(
+ "Zagłodzenie i Inwersja priorytetów", fontsize=FS_TITLE, fontweight="bold"
+ )
+
+ # Panel 1: Starvation + aging
+ ax = axes[0]
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 6)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("Zagłodzenie (Starvation)", fontsize=FS_LABEL, fontweight="bold")
+
+ threads = [
+ ("Wątek HIGH", "prio=10", GRAY5, 3.0),
+ ("Wątek HIGH", "prio=9", GRAY3, 2.2),
+ ("Wątek MED", "prio=5", GRAY2, 1.4),
+ ("Wątek LOW", "prio=1 → głoduje!", "#F8D7DA", 0.6),
+ ]
+ for name, prio, color, y in threads:
+ draw_box(
+ ax, 0.5, y, 2.0, 0.6, name, fill=color, fontsize=FS_SMALL, fontweight="bold"
+ )
+ ax.text(2.8, y + 0.3, prio, fontsize=FS_SMALL, va="center")
+
+ ax.text(
+ 1.5,
+ 4.2,
+ "CPU zawsze\ndostaje HIGH!",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ )
+ draw_arrow(ax, 1.5, 3.9, 1.5, 3.65, lw=1.5)
+
+ # Aging solution
+ draw_box(ax, 4.5, 1.5, 3.2, 2.5, "", fill=GRAY4, rounded=True)
+ ax.text(6.1, 3.7, "Rozwiązanie: AGING", fontsize=FS, fontweight="bold", ha="center")
+ aging = [
+ "t=0: prio=1",
+ "t=100ms: prio=2",
+ "t=200ms: prio=3",
+ "...",
+ "w końcu → CPU!",
+ ]
+ for i, line in enumerate(aging):
+ ax.text(
+ 6.1, 3.2 - i * 0.4, line, fontsize=FS_SMALL, ha="center", family="monospace"
+ )
+
+ # Panel 2: Priority Inversion
+ ax = axes[1]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 6)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title("Inwersja priorytetów", fontsize=FS_LABEL, fontweight="bold")
+
+ # Timeline
+ labels = ["H (wysoki)", "M (średni)", "L (niski)"]
+ ys = [4.2, 2.8, 1.4]
+ for label, y in zip(labels, ys, strict=False):
+ ax.text(0.3, y + 0.2, label, fontsize=FS, fontweight="bold", va="center")
+
+ # L runs and locks mutex
+ draw_box(ax, 2.0, ys[2], 1.2, 0.5, "lock(m)", fill=GRAY1, fontsize=FS_SMALL)
+
+ # M preempts L
+ draw_box(ax, 3.5, ys[1], 3.0, 0.5, "M pracuje...", fill=GRAY3, fontsize=FS_SMALL)
+
+ # H waits for mutex
+ draw_box(
+ ax,
+ 3.5,
+ ys[0],
+ 3.0,
+ 0.5,
+ "CZEKA na mutex!",
+ fill="#F8D7DA",
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+
+ # M finishes, L continues, unlocks
+ draw_box(ax, 6.8, ys[2], 1.5, 0.5, "unlock(m)", fill=GRAY1, fontsize=FS_SMALL)
+ draw_box(ax, 8.5, ys[0], 1.2, 0.5, "H runs", fill=GRAY4, fontsize=FS_SMALL)
+
+ # Explanation
+ ax.text(
+ 5.0,
+ 0.5,
+ "H czeka na M (mimo H > M)!\n"
+ "Rozwiązanie: Priority Inheritance\n"
+ "L dziedziczy priorytet H → M nie wypycha L",
+ fontsize=FS_SMALL,
+ ha="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ ax.text(
+ 5.0,
+ 0.0,
+ "Mars Pathfinder (1997) — klasyczny bug!",
+ fontsize=FS_SMALL,
+ ha="center",
+ fontweight="bold",
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.9])
+ save_fig(fig, "q9_starvation_priority.png")
+
+
+# ============================================================
+# 14. Bounded buffer + readers-writers + philosophers
+# ============================================================
+def gen_classic_problems() -> None:
+ """Gen classic problems."""
+ fig, axes = plt.subplots(1, 3, figsize=(12, 5))
+ fig.suptitle(
+ "Klasyczne problemy synchronizacji", fontsize=FS_TITLE, fontweight="bold"
+ )
+
+ # Panel 1: Bounded Buffer with semaphores
+ ax = axes[0]
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Producent-Konsument\n(Bounded Buffer, N=4)", fontsize=FS, fontweight="bold"
+ )
+
+ draw_box(
+ ax,
+ 0.2,
+ 4.0,
+ 2.0,
+ 1.2,
+ "Producent\nP(empty)\nP(mutex)\nwstaw()\nV(mutex)\nV(full)",
+ fill=GRAY1,
+ fontsize=5.5,
+ )
+ # Buffer
+ items = ["A", "B", "", ""]
+ for i, item in enumerate(items):
+ x = 2.8 + i * 0.9
+ fill = GRAY3 if item else "white"
+ draw_box(
+ ax,
+ x,
+ 4.3,
+ 0.9,
+ 0.7,
+ item,
+ fill=fill,
+ fontsize=FS,
+ fontweight="bold",
+ rounded=False,
+ )
+ ax.text(4.6, 5.2, "Bufor (N=4)", fontsize=FS_SMALL, ha="center", fontweight="bold")
+ draw_box(
+ ax,
+ 6.0,
+ 4.0,
+ 2.0,
+ 1.2,
+ "Konsument\nP(full)\nP(mutex)\npobierz()\nV(mutex)\nV(empty)",
+ fill=GRAY4,
+ fontsize=5.5,
+ )
+ draw_arrow(ax, 2.2, 4.6, 2.8, 4.65, lw=1.2)
+ draw_arrow(ax, 6.4, 4.65, 6.0, 4.6, lw=1.2)
+
+ # Semaphores
+ sems = [("mutex = 1", GRAY2), ("empty = N", GRAY1), ("full = 0", GRAY3)]
+ for i, (s, c) in enumerate(sems):
+ draw_box(
+ ax,
+ 2.0,
+ 2.5 - i * 0.6,
+ 4.0,
+ 0.45,
+ s,
+ fill=c,
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+
+ ax.text(
+ 4.0,
+ 0.5,
+ "KOLEJNOŚĆ: P(empty/full)\nPRZED P(mutex)!\nOdwrotnie = DEADLOCK",
+ fontsize=5.5,
+ ha="center",
+ fontweight="bold",
+ color="#C62828",
+ bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
+ )
+
+ # Panel 2: Readers-Writers
+ ax = axes[1]
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Czytelnicy-Pisarze\n(Readers-Writers)", fontsize=FS, fontweight="bold"
+ )
+
+ # Resource
+ draw_box(
+ ax,
+ 2.5,
+ 3.5,
+ 3.0,
+ 1.5,
+ "Dane\n(współdzielone)",
+ fill=GRAY2,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Readers
+ for i in range(3):
+ x = 0.3 + i * 1.0
+ draw_box(
+ ax,
+ x,
+ 5.5,
+ 0.8,
+ 0.7,
+ f"R{i + 1}",
+ fill=GRAY4,
+ fontsize=FS,
+ fontweight="bold",
+ )
+ draw_arrow(ax, x + 0.4, 5.5, 3.0 + i * 0.5, 5.0, lw=1)
+
+ ax.text(
+ 1.5,
+ 6.5,
+ "Czytelnicy (wielu naraz)",
+ fontsize=FS_SMALL,
+ ha="center",
+ fontweight="bold",
+ )
+
+ # Writer
+ draw_box(
+ ax, 5.5, 5.5, 1.5, 0.7, "Pisarz", fill=GRAY5, fontsize=FS, fontweight="bold"
+ )
+ draw_arrow(ax, 6.25, 5.5, 5.0, 5.0, lw=1.5)
+ ax.text(
+ 6.25,
+ 6.5,
+ "WYŁĄCZNY",
+ fontsize=FS_SMALL,
+ ha="center",
+ fontweight="bold",
+ color="#C62828",
+ )
+
+ # Rules
+ rules = [
+ "Wielu czytelników = OK",
+ "Jeden pisarz = wyłączny",
+ "Czytelnik + Pisarz = ✗",
+ "Problem: pisarze głodują",
+ ]
+ for i, r in enumerate(rules):
+ ax.text(4.0, 2.5 - i * 0.45, r, fontsize=FS_SMALL, ha="center")
+
+ ax.text(
+ 4.0,
+ 0.5,
+ "Rozwiązanie:\nrw_mutex + count_mutex\n+ zmienna readers",
+ fontsize=5.5,
+ ha="center",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # Panel 3: Dining Philosophers
+ ax = axes[2]
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Ucztujący filozofowie\n(Dining Philosophers)", fontsize=FS, fontweight="bold"
+ )
+
+ # Draw circular table
+ cx, cy, r = 4.0, 3.8, 1.8
+ table = plt.Circle((cx, cy), 0.8, fill=True, facecolor=GRAY2, edgecolor=LN, lw=1.5)
+ ax.add_patch(table)
+ ax.text(cx, cy, "Stół", fontsize=FS, ha="center", fontweight="bold")
+
+ # 5 philosophers around table
+ for i in range(5):
+ angle = np.pi / 2 + i * 2 * np.pi / 5
+ px = cx + r * np.cos(angle)
+ py = cy + r * np.sin(angle)
+ circle = plt.Circle(
+ (px, py), 0.35, fill=True, facecolor=GRAY1, edgecolor=LN, lw=1.2
+ )
+ ax.add_patch(circle)
+ ax.text(
+ px, py, f"F{i}", ha="center", va="center", fontsize=FS, fontweight="bold"
+ )
+
+ # Fork between philosophers
+ fork_angle = np.pi / 2 + (i + 0.5) * 2 * np.pi / 5
+ fx = cx + (r * 0.6) * np.cos(fork_angle)
+ fy = cy + (r * 0.6) * np.sin(fork_angle)
+ ax.plot(
+ [fx - 0.1, fx + 0.1],
+ [fy - 0.15, fy + 0.15],
+ color=LN,
+ lw=2.5,
+ solid_capstyle="round",
+ )
+ ax.text(fx + 0.2, fy + 0.15, f"w{i}", fontsize=5, color="#555555")
+
+ # Rules
+ rules = [
+ "Jedzenie = 2 widelce",
+ "Naiwne → DEADLOCK",
+ "Fix: F4 bierze odwrotnie",
+ "Alt: semafor(4)",
+ ]
+ for i, r in enumerate(rules):
+ ax.text(4.0, 1.2 - i * 0.35, r, fontsize=FS_SMALL, ha="center")
+
+ fig.tight_layout(rect=[0, 0, 1, 0.88])
+ save_fig(fig, "q9_classic_problems.png")
+
+
+# ============================================================
+# 15. Sync mechanisms comparison + mutex/sem/spinlock
+# ============================================================
+def gen_sync_comparison() -> None:
+ """Gen sync comparison."""
+ fig, axes = plt.subplots(2, 1, figsize=(9, 7))
+
+ # Top: comparison table
+ ax = axes[0]
+ ax.set_xlim(0, 11.5)
+ ax.set_ylim(-5, 1)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Mechanizmy synchronizacji — porównanie",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ headers = ["Mechanizm", "Opis", "Kiedy używać"]
+ col_w = [2.5, 4.5, 4.0]
+ rows = [
+ ["Mutex", "Zamek: 1 wątek w sekcji", "Sekcja krytyczna"],
+ ["Semafor(n)", "Licznik: max n wątków", "Ograniczone zasoby (n miejsc)"],
+ ["Monitor", "Obiekt z wbudowanym mutex", "Java synchronized"],
+ ["Cond. Variable", "wait()/signal() na warunek", "Producent-konsument"],
+ ["Spinlock", "Aktywne czekanie (busy-wait)", "Bardzo krótkie sekcje (<1 μs)"],
+ ["RW Lock", "Wielu czytelników LUB 1 pisarz", "Bazy danych, cache"],
+ ["Barrier", "Czekaj aż wszyscy dotrą", "Obliczenia równoległe"],
+ ]
+ draw_table(
+ ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.5, fontsize=7
+ )
+
+ # Bottom: mutex vs semafor vs spinlock
+ ax = axes[1]
+ ax.set_xlim(0, 12)
+ ax.set_ylim(0, 5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Mutex vs Semafor vs Spinlock", fontsize=FS_TITLE, fontweight="bold", pad=5
+ )
+
+ # Mutex
+ draw_box(ax, 0.3, 2.5, 3.5, 2.0, "", fill=GRAY4)
+ ax.text(2.05, 4.2, "MUTEX", fontsize=FS_LABEL, fontweight="bold", ha="center")
+ ax.text(2.05, 3.6, "= klucz do łazienki\n(1 osoba)", fontsize=FS, ha="center")
+ ax.text(
+ 2.05,
+ 2.8,
+ "Wątek ZASYPIA gdy czeka\nOS go obudzi (~μs)",
+ fontsize=FS_SMALL,
+ ha="center",
+ style="italic",
+ )
+
+ # Semafor
+ draw_box(ax, 4.3, 2.5, 3.5, 2.0, "", fill=GRAY1)
+ ax.text(6.05, 4.2, "SEMAFOR(n)", fontsize=FS_LABEL, fontweight="bold", ha="center")
+ ax.text(
+ 6.05, 3.6, "= parking na n miejsc\n(n wątków naraz)", fontsize=FS, ha="center"
+ )
+ ax.text(
+ 6.05,
+ 2.8,
+ "Semafor(1) = mutex\nP() = zmniejsz, V() = zwiększ",
+ fontsize=FS_SMALL,
+ ha="center",
+ style="italic",
+ )
+
+ # Spinlock
+ draw_box(ax, 8.3, 2.5, 3.5, 2.0, "", fill=GRAY2)
+ ax.text(10.05, 4.2, "SPINLOCK", fontsize=FS_LABEL, fontweight="bold", ha="center")
+ ax.text(
+ 10.05, 3.6, "= obrotowe drzwi\n(kręcisz się w kółko)", fontsize=FS, ha="center"
+ )
+ ax.text(
+ 10.05,
+ 2.8,
+ "Wątek KRĘCI się w pętli\nLepszy gdy sekcja < 1 μs",
+ fontsize=FS_SMALL,
+ ha="center",
+ style="italic",
+ )
+
+ # Dividing rule
+ ax.text(
+ 6.0,
+ 1.5,
+ "Reguła kciuka: sekcja > 1 μs → MUTEX | "
+ "sekcja < 1 μs → SPINLOCK | n jednocześnie → SEMAFOR(n)",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ fig.tight_layout()
+ save_fig(fig, "q9_sync_comparison.png")
+
+
+# ============================================================
+# 16. Semaphore concept diagram
+# ============================================================
+def gen_semaphore_concept() -> None:
+ """Gen semaphore concept."""
+ fig, ax = plt.subplots(figsize=(6, 3))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 5)
+ ax.set_aspect("auto")
+ ax.axis("off")
+ ax.set_title(
+ "Semafor — koncepcja (parking na 3 miejsca)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Parking slots
+ for i in range(3):
+ x = 2.0 + i * 2.0
+ occupied = i < 2 # 2 occupied, 1 free
+ fill = GRAY3 if occupied else "white"
+ label = f"Wątek {i + 1}" if occupied else "(wolne)"
+ draw_box(
+ ax,
+ x,
+ 2.5,
+ 1.5,
+ 1.2,
+ label,
+ fill=fill,
+ fontsize=FS,
+ fontweight="bold" if occupied else "normal",
+ rounded=False,
+ )
+
+ ax.text(
+ 5.0,
+ 4.2,
+ "semafor(3): counter = 1 (jedno wolne miejsce)",
+ fontsize=FS,
+ ha="center",
+ fontweight="bold",
+ )
+
+ # Waiting thread
+ draw_box(
+ ax,
+ 0.2,
+ 0.5,
+ 1.5,
+ 0.8,
+ "Wątek 4\nP() → czeka",
+ fill="#F8D7DA",
+ fontsize=FS_SMALL,
+ )
+ draw_arrow(ax, 1.7, 0.9, 2.0, 2.5, lw=1.2, color="#C62828")
+
+ ax.text(
+ 5.0,
+ 0.6,
+ "P() = counter-- (jeśli 0 → czekaj)\nV() = counter++ (obudź czekającego)",
+ fontsize=FS,
+ ha="center",
+ family="monospace",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ save_fig(fig, "q9_semaphore_concept.png")
+
+
+# ============================================================
+# MAIN — generate all
+# ============================================================
+if __name__ == "__main__":
+ print("Generating ALL PYTANIE 9 diagrams...")
+ gen_process_vs_thread()
+ gen_memory_layout()
+ gen_process_states()
+ gen_thread_structure()
+ gen_pcb_structure()
+ gen_speed_comparison()
+ gen_scenario_table()
+ gen_ipc_details()
+ gen_ipc_table()
+ gen_race_condition()
+ gen_deadlock_scenario()
+ gen_coffman_strategies()
+ gen_starvation_priority()
+ gen_classic_problems()
+ gen_sync_comparison()
+ gen_semaphore_concept()
+ print("\nAll 16 PYTANIE 9 diagrams generated successfully!")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_q9_q12_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_q9_q12_diagrams.py
new file mode 100755
index 0000000..2e83f36
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_q9_q12_diagrams.py
@@ -0,0 +1,1132 @@
+#!/usr/bin/env python3
+"""Generate diagrams for PYTANIE 9 and PYTANIE 12.
+
+ PYTANIE 9: Processes & Threads (IPC mechanisms, deadlock, producer-consumer)
+ PYTANIE 12: Network optimization models (Ford-Fulkerson, Hungarian, CPM, Kruskal, TSP, Min-cost flow).
+
+All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+import numpy as np
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 11
+FS_SMALL = 6.5
+FS_EDGE = 9
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+LIGHT_GREEN = "#D5E8D4"
+LIGHT_RED = "#F8D7DA"
+LIGHT_BLUE = "#D6EAF8"
+LIGHT_YELLOW = "#FFF9C4"
+LIGHT_ORANGE = "#FFE0B2"
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.2,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=True,
+ edgecolor=LN,
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y),
+ w,
+ h,
+ boxstyle="round,pad=0.05",
+ lw=lw,
+ edgecolor=edgecolor,
+ facecolor=fill,
+ )
+ else:
+ rect = mpatches.Rectangle(
+ (x, y), w, h, lw=lw, edgecolor=edgecolor, facecolor=fill
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ )
+
+
+def save_fig(fig, name) -> None:
+ """Save fig."""
+ path = str(Path(OUTPUT_DIR) / name)
+ fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG, pad_inches=0.15)
+ plt.close(fig)
+ print(f" Saved: {path}")
+
+
+# ============================================================
+# PYTANIE 9 DIAGRAMS
+# ============================================================
+
+
+def gen_ipc_mechanisms() -> None:
+ """IPC mechanisms comparison diagram."""
+ fig, ax = plt.subplots(1, 1, figsize=(8, 5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Mechanizmy IPC — porównanie", fontsize=FS_TITLE, fontweight="bold", pad=10
+ )
+
+ mechanisms = [
+ (
+ "Pipe",
+ "→ jednokierunkowy\n→ bufor w jądrze\n→ spokrewnione procesy",
+ "ls | grep txt",
+ GRAY1,
+ ),
+ (
+ "Shared\nMemory",
+ "→ wspólna ramka RAM\n→ zero kopiowania\n→ wymaga synchronizacji",
+ "mmap() / shm_open()",
+ LIGHT_GREEN,
+ ),
+ (
+ "Message\nQueue",
+ "→ strukturalne wiad.\n→ asynchroniczna\n→ filtrowanie typów",
+ "msgsnd() / msgrcv()",
+ LIGHT_BLUE,
+ ),
+ (
+ "Socket",
+ "→ dwukierunkowy\n→ lokalny lub sieciowy\n→ TCP/UDP",
+ "connect() / accept()",
+ LIGHT_YELLOW,
+ ),
+ ]
+
+ for i, (name, desc, example, color) in enumerate(mechanisms):
+ x = 0.3
+ y = 5.5 - i * 1.5
+ # Box for mechanism name
+ draw_box(ax, x, y, 1.5, 1.0, name, fill=color, fontsize=9, fontweight="bold")
+ # Description
+ ax.text(
+ x + 2.0,
+ y + 0.5,
+ desc,
+ fontsize=FS,
+ va="center",
+ ha="left",
+ family="monospace",
+ )
+ # Example
+ draw_box(ax, 6.5, y + 0.15, 3.0, 0.7, example, fill=GRAY4, fontsize=FS_SMALL)
+
+ # Draw process boxes for pipe illustration at top
+ y_top = 6.3
+ ax.text(
+ 5.0,
+ y_top,
+ "Proces A ──bufor jądra──▶ Proces B",
+ fontsize=FS,
+ ha="center",
+ va="center",
+ family="monospace",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY1, "edgecolor": GRAY3},
+ )
+
+ # Legend
+ ax.text(
+ 0.3,
+ 0.3,
+ "Szybkość: Shared Memory > Pipe ≈ MsgQueue > Socket (sieciowy)",
+ fontsize=FS,
+ va="center",
+ style="italic",
+ )
+
+ save_fig(fig, "ipc_mechanisms.png")
+
+
+def gen_deadlock_illustration() -> None:
+ """Deadlock circular wait diagram."""
+ fig, ax = plt.subplots(1, 1, figsize=(6, 5))
+ ax.set_xlim(0, 8)
+ ax.set_ylim(0, 6.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Zakleszczenie (Deadlock) — cykliczne oczekiwanie",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Thread boxes
+ draw_box(
+ ax,
+ 0.5,
+ 3.5,
+ 2.0,
+ 1.2,
+ "Wątek A\n(trzyma Mutex 1)",
+ fill=LIGHT_BLUE,
+ fontsize=9,
+ fontweight="bold",
+ )
+ draw_box(
+ ax,
+ 5.5,
+ 3.5,
+ 2.0,
+ 1.2,
+ "Wątek B\n(trzyma Mutex 2)",
+ fill=LIGHT_ORANGE,
+ fontsize=9,
+ fontweight="bold",
+ )
+
+ # Resource boxes
+ draw_box(
+ ax,
+ 0.5,
+ 0.8,
+ 2.0,
+ 1.0,
+ "Mutex 1\nzablokowany",
+ fill=GRAY2,
+ fontsize=8,
+ fontweight="bold",
+ )
+ draw_box(
+ ax,
+ 5.5,
+ 0.8,
+ 2.0,
+ 1.0,
+ "Mutex 2\nzablokowany",
+ fill=GRAY2,
+ fontsize=8,
+ fontweight="bold",
+ )
+
+ # Arrows: "holds" (down)
+ draw_arrow(ax, 1.5, 3.5, 1.5, 1.8, lw=2.0, color="#333333")
+ ax.text(0.3, 2.65, "trzyma", fontsize=FS, ha="center", rotation=90, color="#333333")
+
+ draw_arrow(ax, 6.5, 3.5, 6.5, 1.8, lw=2.0, color="#333333")
+ ax.text(7.7, 2.65, "trzyma", fontsize=FS, ha="center", rotation=90, color="#333333")
+
+ # Arrows: "waits for" (across, with red)
+ draw_arrow(ax, 2.5, 4.3, 5.5, 4.3, lw=2.5, color="#C62828")
+ ax.text(
+ 4.0,
+ 4.6,
+ "czeka na Mutex 2",
+ fontsize=FS,
+ ha="center",
+ color="#C62828",
+ fontweight="bold",
+ )
+
+ draw_arrow(ax, 5.5, 3.7, 2.5, 3.7, lw=2.5, color="#C62828")
+ ax.text(
+ 4.0,
+ 3.2,
+ "czeka na Mutex 1",
+ fontsize=FS,
+ ha="center",
+ color="#C62828",
+ fontweight="bold",
+ )
+
+ # Coffman conditions
+ conditions = [
+ "1. Mutual Exclusion — zasoby wyłączne",
+ "2. Hold and Wait — trzymaj + czekaj",
+ "3. No Preemption — nie można zabrać siłą",
+ "4. Circular Wait — cykl oczekiwania ← złam ten!",
+ ]
+ for i, cond in enumerate(conditions):
+ color_c = "#C62828" if i == 3 else LN
+ fw = "bold" if i == 3 else "normal"
+ ax.text(
+ 0.5,
+ 0.5 - i * 0.25 + 0.2,
+ cond,
+ fontsize=FS_SMALL,
+ color=color_c,
+ fontweight=fw,
+ va="center",
+ )
+
+ save_fig(fig, "deadlock_illustration.png")
+
+
+def gen_producer_consumer() -> None:
+ """Producer-consumer with bounded buffer diagram."""
+ fig, ax = plt.subplots(1, 1, figsize=(8, 4.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 5.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Producent-Konsument z buforem cyklicznym (N=4)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Producer
+ draw_box(
+ ax,
+ 0.3,
+ 2.0,
+ 1.8,
+ 1.5,
+ "Producent\n\nwstaw(elem)\nV(full)\nV(mutex)",
+ fill=LIGHT_GREEN,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Buffer slots
+ buf_x = 3.0
+ buf_y = 2.5
+ slot_w = 1.0
+ slot_h = 0.8
+ items = ["A", "B", "", ""]
+ fills = [LIGHT_BLUE, LIGHT_BLUE, "white", "white"]
+ for i, (item, fc) in enumerate(zip(items, fills, strict=False)):
+ x = buf_x + i * slot_w
+ draw_box(
+ ax,
+ x,
+ buf_y,
+ slot_w,
+ slot_h,
+ item,
+ fill=fc,
+ fontsize=10,
+ fontweight="bold",
+ rounded=False,
+ )
+
+ ax.text(
+ buf_x + 2.0,
+ buf_y + slot_h + 0.3,
+ "Bufor (N=4)",
+ fontsize=9,
+ ha="center",
+ fontweight="bold",
+ )
+ ax.text(
+ buf_x + 2.0,
+ buf_y - 0.3,
+ "full=2, empty=2",
+ fontsize=FS,
+ ha="center",
+ family="monospace",
+ )
+
+ # Consumer
+ draw_box(
+ ax,
+ 7.8,
+ 2.0,
+ 1.8,
+ 1.5,
+ "Konsument\n\npobierz()\nV(empty)\nV(mutex)",
+ fill=LIGHT_YELLOW,
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ # Arrows
+ draw_arrow(ax, 2.1, 2.75, 3.0, 2.9, lw=1.5)
+ draw_arrow(ax, 7.0, 2.9, 7.8, 2.75, lw=1.5)
+
+ # Semaphores
+ sems = [
+ ("mutex = 1", "wyłączny dostęp do bufora", GRAY2),
+ ("empty = 2", "wolne sloty (P = czekaj, V = +1)", LIGHT_GREEN),
+ ("full = 2", "pełne sloty (P = czekaj, V = +1)", LIGHT_BLUE),
+ ]
+ for i, (name, desc, color) in enumerate(sems):
+ y = 1.2 - i * 0.45
+ draw_box(
+ ax,
+ 3.0,
+ y,
+ 1.5,
+ 0.35,
+ name,
+ fill=color,
+ fontsize=FS_SMALL,
+ fontweight="bold",
+ )
+ ax.text(4.7, y + 0.17, desc, fontsize=FS_SMALL, va="center")
+
+ # Warning
+ ax.text(
+ 0.3,
+ 4.8,
+ "KOLEJNOŚĆ: P(empty/full) PRZED P(mutex)! Odwrotnie = DEADLOCK",
+ fontsize=FS,
+ fontweight="bold",
+ color="#C62828",
+ bbox={
+ "boxstyle": "round,pad=0.2",
+ "facecolor": LIGHT_RED,
+ "edgecolor": "#C62828",
+ },
+ )
+
+ save_fig(fig, "producer_consumer.png")
+
+
+# ============================================================
+# PYTANIE 12 DIAGRAMS
+# ============================================================
+
+
+def draw_network_node(ax, name, pos, color="white", fontsize=10, r=0.3) -> None:
+ """Draw a network node (circle)."""
+ x, y = pos
+ circle = plt.Circle(
+ (x, y), r, fill=True, facecolor=color, edgecolor=LN, linewidth=1.5, zorder=5
+ )
+ ax.add_patch(circle)
+ ax.text(
+ x,
+ y,
+ name,
+ ha="center",
+ va="center",
+ fontsize=fontsize,
+ fontweight="bold",
+ zorder=6,
+ )
+
+
+def draw_network_edge(
+ ax,
+ pos1,
+ pos2,
+ label="",
+ color=LN,
+ lw=1.5,
+ offset=0.0,
+ directed=True,
+ r=0.33,
+ label_bg="white",
+) -> None:
+ """Draw a directed edge with label."""
+ x1, y1 = pos1
+ x2, y2 = pos2
+ dx, dy = x2 - x1, y2 - y1
+ length = np.sqrt(dx**2 + dy**2)
+ if length == 0:
+ return
+ sx = x1 + r * dx / length
+ sy = y1 + r * dy / length
+ ex = x2 - r * dx / length
+ ey = y2 - r * dy / length
+
+ if directed:
+ ax.annotate(
+ "",
+ xy=(ex, ey),
+ xytext=(sx, sy),
+ arrowprops={"arrowstyle": "->", "color": color, "lw": lw},
+ )
+ else:
+ ax.plot([sx, ex], [sy, ey], color=color, linewidth=lw, zorder=2)
+
+ if label:
+ mx = (x1 + x2) / 2
+ my = (y1 + y2) / 2
+ perp_x = -dy / length * (0.2 + offset)
+ perp_y = dx / length * (0.2 + offset)
+ ax.text(
+ mx + perp_x,
+ my + perp_y,
+ str(label),
+ ha="center",
+ va="center",
+ fontsize=FS_EDGE,
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.1",
+ "facecolor": label_bg,
+ "edgecolor": GRAY3,
+ "alpha": 0.95,
+ },
+ zorder=4,
+ )
+
+
+def gen_ford_fulkerson() -> None:
+ """Ford-Fulkerson max flow step-by-step."""
+ fig, axes = plt.subplots(2, 2, figsize=(10, 8))
+ fig.suptitle(
+ "Ford-Fulkerson — Maksymalny przepływ (krok po kroku)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ )
+
+ pos = {"s": (0.5, 1.5), "A": (2.5, 2.5), "B": (2.5, 0.5), "t": (4.5, 1.5)}
+
+ steps = [
+ {
+ "title": "Krok 0: Sieć wejściowa\n(przepustowości)",
+ "edges": [
+ ("s", "A", "10"),
+ ("s", "B", "8"),
+ ("A", "t", "6"),
+ ("B", "t", "10"),
+ ("B", "A", "2"),
+ ],
+ "flows": {},
+ "path": [],
+ "note": "Szukamy ścieżki s→...→t",
+ },
+ {
+ "title": "Krok 1: Ścieżka s→A→t\nPrzepływ: +6 (min(10,6))",
+ "edges": [
+ ("s", "A", "4/10"),
+ ("s", "B", "0/8"),
+ ("A", "t", "6/6"),
+ ("B", "t", "0/10"),
+ ("B", "A", "0/2"),
+ ],
+ "flows": {},
+ "path": [("s", "A"), ("A", "t")],
+ "note": "Łączny przepływ: 6",
+ },
+ {
+ "title": "Krok 2: Ścieżka s→B→t\nPrzepływ: +8 (min(8,10))",
+ "edges": [
+ ("s", "A", "4/10"),
+ ("s", "B", "8/8"),
+ ("A", "t", "6/6"),
+ ("B", "t", "8/10"),
+ ("B", "A", "0/2"),
+ ],
+ "flows": {},
+ "path": [("s", "B"), ("B", "t")],
+ "note": "Łączny przepływ: 14",
+ },
+ {
+ "title": "Krok 3: Brak ścieżki powiększającej\nMAX FLOW = 14",
+ "edges": [
+ ("s", "A", "4/10"),
+ ("s", "B", "8/8"),
+ ("A", "t", "6/6"),
+ ("B", "t", "8/10"),
+ ("B", "A", "0/2"),
+ ],
+ "flows": {},
+ "path": [],
+ "note": "Min-cut: {s,A,B}|{t}\nA→t(6)+B→t(10)=16? Nie!\ns→B(8)+A→t(6)=14 ✓",
+ },
+ ]
+
+ for _idx, (ax, step) in enumerate(zip(axes.flat, steps, strict=False)):
+ ax.set_xlim(-0.3, 5.3)
+ ax.set_ylim(-0.3, 3.3)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(step["title"], fontsize=FS, fontweight="bold", pad=5)
+
+ path_set = set(step["path"])
+
+ for e in step["edges"]:
+ u, v, label = e
+ is_path = (u, v) in path_set
+ c = "#C62828" if is_path else LN
+ w = 2.5 if is_path else 1.5
+ draw_network_edge(ax, pos[u], pos[v], label=label, color=c, lw=w)
+
+ for name, p in pos.items():
+ if name == "s":
+ c = LIGHT_GREEN
+ elif name == "t":
+ c = LIGHT_RED
+ else:
+ c = "white"
+ draw_network_node(ax, name, p, color=c)
+
+ ax.text(
+ 2.5,
+ -0.15,
+ step["note"],
+ fontsize=FS_SMALL,
+ ha="center",
+ va="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.93])
+ save_fig(fig, "ford_fulkerson_example.png")
+
+
+def gen_hungarian() -> None:
+ """Hungarian algorithm step-by-step."""
+ fig, axes = plt.subplots(2, 2, figsize=(9, 7))
+ fig.suptitle(
+ "Algorytm węgierski — Problem przydziału (krok po kroku)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ )
+
+ matrices = [
+ {
+ "title": "Macierz kosztów (wejściowa)",
+ "data": [[8, 4, 7], [5, 2, 3], [9, 4, 8]],
+ "highlight": [],
+ "note": "Minimalizuj łączny koszt przydziału",
+ },
+ {
+ "title": "Krok 1: Redukcja wierszy\n(odejmij min z wiersza)",
+ "data": [[4, 0, 3], [3, 0, 1], [5, 0, 4]],
+ "highlight": [(0, 1), (1, 1), (2, 1)],
+ "note": "min: A=4, B=2, C=4",
+ },
+ {
+ "title": "Krok 2: Redukcja kolumn\n(odejmij min z kolumny)",
+ "data": [[1, 0, 2], [0, 0, 0], [2, 0, 3]],
+ "highlight": [(1, 0), (0, 1), (1, 1), (2, 1), (1, 2)],
+ "note": "min: Z1=3, Z2=0, Z3=1",
+ },
+ {
+ "title": "Krok 3: Optymalne przypisanie\nA→Z2(4), B→Z1(5), C=?",
+ "data": [[0, 0, 1], [0, 1, 0], [1, 0, 2]],
+ "highlight": [(0, 1), (1, 0), (2, 1)],
+ "note": "Optymalne: A→Z1(8) + B→Z3(3) + C→Z2(4) = 15",
+ },
+ ]
+
+ rows = ["A", "B", "C"]
+ cols = ["Z1", "Z2", "Z3"]
+
+ for ax, m in zip(axes.flat, matrices, strict=False):
+ ax.set_xlim(-0.5, 4.5)
+ ax.set_ylim(-1, 4.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(m["title"], fontsize=FS, fontweight="bold", pad=5)
+
+ # Column headers
+ for j, col in enumerate(cols):
+ ax.text(
+ j + 1.5,
+ 3.8,
+ col,
+ ha="center",
+ va="center",
+ fontsize=9,
+ fontweight="bold",
+ )
+
+ # Row headers and data
+ for i, row in enumerate(rows):
+ y = 2.8 - i
+ ax.text(
+ 0.3, y, row, ha="center", va="center", fontsize=9, fontweight="bold"
+ )
+ for j in range(3):
+ val = m["data"][i][j]
+ is_zero = val == 0
+ is_hl = (i, j) in m["highlight"]
+ fc = (
+ LIGHT_GREEN if is_hl else ("white" if not is_zero else LIGHT_YELLOW)
+ )
+ rect = FancyBboxPatch(
+ (j + 1.0, y - 0.35),
+ 1.0,
+ 0.7,
+ boxstyle="round,pad=0.05",
+ lw=1.2,
+ edgecolor=LN if not is_hl else "#1B5E20",
+ facecolor=fc,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ j + 1.5,
+ y,
+ str(val),
+ ha="center",
+ va="center",
+ fontsize=10,
+ fontweight="bold" if is_hl else "normal",
+ )
+
+ ax.text(
+ 2.0,
+ -0.6,
+ m["note"],
+ fontsize=FS_SMALL,
+ ha="center",
+ va="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.93])
+ save_fig(fig, "hungarian_example.png")
+
+
+def gen_cpm() -> None:
+ """CPM critical path diagram."""
+ fig, ax = plt.subplots(1, 1, figsize=(10, 5))
+ ax.set_xlim(-0.5, 12)
+ ax.set_ylim(-0.5, 5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "CPM — Ścieżka krytyczna projektu IT",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Task positions: (x, y)
+ tasks = {
+ "START": (0.5, 2.5),
+ "A\n3 tyg": (2.5, 2.5),
+ "B\n4 tyg": (5.0, 3.8),
+ "C\n5 tyg": (5.0, 1.2),
+ "D\n6 tyg": (7.5, 3.8),
+ "E\n2 tyg": (9.5, 2.5),
+ "F\n1 tyg": (11.5, 2.5),
+ }
+
+ # Critical path: START→A→B→D→E→F
+ critical = {"START", "A\n3 tyg", "B\n4 tyg", "D\n6 tyg", "E\n2 tyg", "F\n1 tyg"}
+ critical_edges = {
+ ("START", "A\n3 tyg"),
+ ("A\n3 tyg", "B\n4 tyg"),
+ ("B\n4 tyg", "D\n6 tyg"),
+ ("D\n6 tyg", "E\n2 tyg"),
+ ("E\n2 tyg", "F\n1 tyg"),
+ }
+
+ edges = [
+ ("START", "A\n3 tyg"),
+ ("A\n3 tyg", "B\n4 tyg"),
+ ("A\n3 tyg", "C\n5 tyg"),
+ ("B\n4 tyg", "D\n6 tyg"),
+ ("C\n5 tyg", "E\n2 tyg"),
+ ("D\n6 tyg", "E\n2 tyg"),
+ ("E\n2 tyg", "F\n1 tyg"),
+ ]
+
+ # Draw edges
+ for u, v in edges:
+ is_crit = (u, v) in critical_edges
+ c = "#C62828" if is_crit else GRAY3
+ w = 2.5 if is_crit else 1.2
+ draw_network_edge(ax, tasks[u], tasks[v], color=c, lw=w, r=0.5)
+
+ # Draw nodes
+ for name, p in tasks.items():
+ is_crit = name in critical
+ c = LIGHT_RED if is_crit else LIGHT_BLUE
+ r = 0.45
+ circle = plt.Circle(
+ p,
+ r,
+ fill=True,
+ facecolor=c,
+ edgecolor="#C62828" if is_crit else LN,
+ linewidth=2.0 if is_crit else 1.2,
+ zorder=5,
+ )
+ ax.add_patch(circle)
+ ax.text(
+ p[0],
+ p[1],
+ name,
+ ha="center",
+ va="center",
+ fontsize=7 if "\n" in name else 8,
+ fontweight="bold",
+ zorder=6,
+ )
+
+ # ES/EF labels
+ es_ef = [
+ ("A\n3 tyg", "ES=0, EF=3"),
+ ("B\n4 tyg", "ES=3, EF=7"),
+ ("C\n5 tyg", "ES=3, EF=8\nzapas=5"),
+ ("D\n6 tyg", "ES=7, EF=13"),
+ ("E\n2 tyg", "ES=13, EF=15"),
+ ("F\n1 tyg", "ES=15, EF=16"),
+ ]
+ for name, label in es_ef:
+ x, y = tasks[name]
+ offset_y = 0.7 if y > 2.5 else -0.7
+ ax.text(
+ x,
+ y + offset_y,
+ label,
+ ha="center",
+ va="center",
+ fontsize=FS_SMALL,
+ bbox={
+ "boxstyle": "round,pad=0.1",
+ "facecolor": "white",
+ "edgecolor": GRAY3,
+ "alpha": 0.95,
+ },
+ )
+
+ # Legend
+ ax.text(
+ 0.5,
+ -0.2,
+ "Ścieżka krytyczna: A→B→D→E→F (16 tyg)",
+ fontsize=9,
+ fontweight="bold",
+ color="#C62828",
+ )
+ ax.text(
+ 0.5,
+ -0.6,
+ "C ma 5 tyg zapasu — może się opóźnić bez wpływu na projekt",
+ fontsize=FS,
+ style="italic",
+ )
+
+ save_fig(fig, "cpm_example.png")
+
+
+def gen_kruskal() -> None:
+ """Kruskal MST construction step-by-step."""
+ fig, axes = plt.subplots(2, 2, figsize=(9, 8))
+ fig.suptitle(
+ "Kruskal — budowa MST krok po kroku", fontsize=FS_TITLE, fontweight="bold"
+ )
+
+ pos = {"A": (0.5, 2.5), "B": (3.0, 2.5), "C": (3.0, 0.5), "D": (0.5, 0.5)}
+
+ all_edges = [
+ ("C", "D", 1),
+ ("A", "C", 2),
+ ("A", "B", 4),
+ ("B", "C", 6),
+ ("B", "D", 7),
+ ("A", "D", 8),
+ ]
+
+ steps = [
+ {
+ "title": "Graf wejściowy\n(6 krawędzi)",
+ "mst": [],
+ "consider": None,
+ "note": "Posortowane: CD(1), AC(2), AB(4), BC(6), BD(7), AD(8)",
+ },
+ {
+ "title": "Krok 1: Dodaj C-D (waga 1)\nNajlżejsza krawędź",
+ "mst": [("C", "D", 1)],
+ "consider": ("C", "D"),
+ "note": "MST = {C-D}, koszt = 1",
+ },
+ {
+ "title": "Krok 2: Dodaj A-C (waga 2)\nA nie w {C,D}",
+ "mst": [("C", "D", 1), ("A", "C", 2)],
+ "consider": ("A", "C"),
+ "note": "MST = {C-D, A-C}, koszt = 3",
+ },
+ {
+ "title": "Krok 3: Dodaj A-B (waga 4)\nB nie w {A,C,D} → KONIEC",
+ "mst": [("C", "D", 1), ("A", "C", 2), ("A", "B", 4)],
+ "consider": ("A", "B"),
+ "note": "MST = {C-D, A-C, A-B}, koszt = 7 ✓",
+ },
+ ]
+
+ for ax, step in zip(axes.flat, steps, strict=False):
+ ax.set_xlim(-0.5, 4.0)
+ ax.set_ylim(-0.5, 3.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(step["title"], fontsize=FS, fontweight="bold", pad=5)
+
+ mst_set = {(u, v) for u, v, _ in step["mst"]}
+
+ for u, v, w in all_edges:
+ in_mst = (u, v) in mst_set or (v, u) in mst_set
+ is_cur = step["consider"] and (
+ (u, v) == step["consider"] or (v, u) == step["consider"]
+ )
+ if is_cur:
+ c, lw = "#C62828", 3.0
+ elif in_mst:
+ c, lw = "#1B5E20", 2.5
+ else:
+ c, lw = GRAY3, 1.0
+ draw_network_edge(
+ ax,
+ pos[u],
+ pos[v],
+ label=str(w),
+ color=c,
+ lw=lw,
+ directed=False,
+ label_bg=LIGHT_GREEN if in_mst else "white",
+ )
+
+ for name, p in pos.items():
+ # Check if in current MST component
+ in_mst = any(name in (u, v) for u, v, _ in step["mst"])
+ c = LIGHT_GREEN if in_mst else "white"
+ draw_network_node(ax, name, p, color=c, r=0.3)
+
+ ax.text(
+ 1.75,
+ -0.3,
+ step["note"],
+ fontsize=FS_SMALL,
+ ha="center",
+ va="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.93])
+ save_fig(fig, "kruskal_example.png")
+
+
+def gen_tsp() -> None:
+ """TSP nearest neighbor heuristic."""
+ fig, axes = plt.subplots(1, 2, figsize=(10, 4.5))
+ fig.suptitle(
+ "TSP — heurystyka Nearest Neighbor (5 miast)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ )
+
+ pos = {
+ "A": (0.5, 3.0),
+ "B": (2.0, 4.0),
+ "C": (4.0, 3.5),
+ "D": (3.5, 1.0),
+ "E": (1.5, 1.5),
+ }
+
+ dist = {
+ ("A", "B"): 20,
+ ("A", "C"): 42,
+ ("A", "D"): 35,
+ ("A", "E"): 12,
+ ("B", "C"): 30,
+ ("B", "D"): 34,
+ ("B", "E"): 10,
+ ("C", "D"): 12,
+ ("C", "E"): 40,
+ ("D", "E"): 25,
+ }
+
+ # Left: full graph with all distances
+ ax = axes[0]
+ ax.set_xlim(-0.5, 5.0)
+ ax.set_ylim(0, 5.0)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Graf pełny (odległości)", fontsize=FS, fontweight="bold")
+
+ for (u, v), d in dist.items():
+ draw_network_edge(
+ ax, pos[u], pos[v], label=str(d), color=GRAY3, lw=0.8, directed=False, r=0.3
+ )
+
+ for name, p in pos.items():
+ draw_network_node(ax, name, p, color=LIGHT_BLUE, r=0.3)
+
+ # Right: NN solution
+ ax = axes[1]
+ ax.set_xlim(-0.5, 5.0)
+ ax.set_ylim(0, 5.0)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Nearest Neighbor (start A)\nTrasa: A→E→B→C→D→A = 99",
+ fontsize=FS,
+ fontweight="bold",
+ )
+
+ nn_path = [
+ ("A", "E", 12),
+ ("E", "B", 10),
+ ("B", "C", 30),
+ ("C", "D", 12),
+ ("D", "A", 35),
+ ]
+ colors = ["#C62828", "#1B5E20", "#1565C0", "#E65100", "#4A148C"]
+
+ for i, (u, v, d) in enumerate(nn_path):
+ draw_network_edge(
+ ax,
+ pos[u],
+ pos[v],
+ label=f"{d}",
+ color=colors[i],
+ lw=2.0,
+ directed=True,
+ r=0.3,
+ )
+ # Step number
+ mx = (pos[u][0] + pos[v][0]) / 2
+ my = (pos[u][1] + pos[v][1]) / 2
+ dx = pos[v][0] - pos[u][0]
+ dy = pos[v][1] - pos[u][1]
+ length = np.sqrt(dx**2 + dy**2)
+ ox = dy / length * 0.45
+ oy = -dx / length * 0.45
+ ax.text(
+ mx + ox,
+ my + oy,
+ f"#{i + 1}",
+ fontsize=FS_SMALL,
+ ha="center",
+ color=colors[i],
+ fontweight="bold",
+ )
+
+ for name, p in pos.items():
+ c = LIGHT_GREEN if name == "A" else LIGHT_BLUE
+ draw_network_node(ax, name, p, color=c, r=0.3)
+
+ fig.tight_layout(rect=[0, 0, 1, 0.9])
+ save_fig(fig, "tsp_nearest_neighbor.png")
+
+
+def gen_min_cost_flow() -> None:
+ """Min-cost flow example."""
+ fig, axes = plt.subplots(1, 2, figsize=(10, 4))
+ fig.suptitle(
+ "Minimalny koszt przepływu — transport 10 ton",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ )
+
+ pos = {"s": (0.5, 1.5), "A": (2.5, 2.5), "B": (2.5, 0.5), "t": (4.5, 1.5)}
+
+ # Left: network with capacities and costs
+ ax = axes[0]
+ ax.set_xlim(-0.3, 5.3)
+ ax.set_ylim(-0.3, 3.3)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Sieć (przepustowość, koszt/t)", fontsize=FS, fontweight="bold")
+
+ edges_info = [
+ ("s", "A", "(8, 2zł)"),
+ ("s", "B", "(5, 4zł)"),
+ ("A", "t", "(6, 3zł)"),
+ ("B", "t", "(5, 1zł)"),
+ ]
+ for u, v, label in edges_info:
+ draw_network_edge(ax, pos[u], pos[v], label=label, r=0.33)
+
+ for name, p in pos.items():
+ c = LIGHT_GREEN if name == "s" else (LIGHT_RED if name == "t" else "white")
+ draw_network_node(ax, name, p, color=c)
+
+ # Right: optimal flow
+ ax = axes[1]
+ ax.set_xlim(-0.3, 5.3)
+ ax.set_ylim(-0.3, 3.3)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Optymalny przepływ (koszt = 50 zł)", fontsize=FS, fontweight="bold")
+
+ opt_edges = [
+ ("s", "A", "5/8", "#1B5E20"),
+ ("s", "B", "5/5", "#C62828"),
+ ("A", "t", "5/6", "#1B5E20"),
+ ("B", "t", "5/5", "#C62828"),
+ ]
+ for u, v, label, color in opt_edges:
+ draw_network_edge(ax, pos[u], pos[v], label=label, color=color, lw=2.0, r=0.33)
+
+ for name, p in pos.items():
+ c = LIGHT_GREEN if name == "s" else (LIGHT_RED if name == "t" else "white")
+ draw_network_node(ax, name, p, color=c)
+
+ ax.text(
+ 2.5,
+ -0.15,
+ "5tx(2+3)=25zł + 5tx(4+1)=25zł = 50zł",
+ fontsize=FS,
+ ha="center",
+ style="italic",
+ bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ fig.tight_layout(rect=[0, 0, 1, 0.9])
+ save_fig(fig, "min_cost_flow_example.png")
+
+
+# ============================================================
+# MAIN
+# ============================================================
+
+if __name__ == "__main__":
+ print("Generating PYTANIE 9 diagrams...")
+ gen_ipc_mechanisms()
+ gen_deadlock_illustration()
+ gen_producer_consumer()
+
+ print("\nGenerating PYTANIE 12 diagrams...")
+ gen_ford_fulkerson()
+ gen_hungarian()
+ gen_cpm()
+ gen_kruskal()
+ gen_tsp()
+ gen_min_cost_flow()
+
+ print("\nAll diagrams generated successfully!")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_robot_lang_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_robot_lang_diagrams.py
new file mode 100755
index 0000000..4258cec
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_robot_lang_diagrams.py
@@ -0,0 +1,942 @@
+#!/usr/bin/env python3
+"""Generate diagrams for PYTANIE 16: Języki programowania robotów.
+
+A4-compatible, B&W, 300 DPI, laser-printer-friendly.
+
+Diagrams:
+ 1. T-R-M-S abstraction pyramid
+ 2. Vendor languages comparison chart
+ 3. Robot movement types (PTP, LIN, CIRC)
+ 4. Online vs Offline programming flowchart
+ 5. ROS architecture (pub/sub nodes)
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+import numpy as np
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 11
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+WHITE = "white"
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.2,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=True,
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y), w, h, boxstyle="round,pad=0.05", lw=lw, edgecolor=LN, facecolor=fill
+ )
+ else:
+ rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ )
+
+
+# ============================================================
+# 1. T-R-M-S Abstraction Pyramid
+# ============================================================
+def draw_trms_pyramid() -> None:
+ """Draw trms pyramid."""
+ fig, ax = plt.subplots(1, 1, figsize=(8.27, 5.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 8)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Poziomy abstrakcji języków programowania robotów (T-R-M-S)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Pyramid layers (bottom to top)
+ layers = [
+ # (y, left_x, right_x, label, sublabel, fill, examples, timing)
+ (
+ 0.5,
+ 1.0,
+ 9.0,
+ "SERVO-LEVEL",
+ "Sterowanie silnikami",
+ GRAY3,
+ "C/C++, FPGA, VHDL\nPID, PWM",
+ "~1 ms",
+ ),
+ (
+ 2.0,
+ 1.8,
+ 8.2,
+ "MOTION-LEVEL",
+ "Planowanie trajektorii",
+ GRAY2,
+ "MoveIt, OMPL\nIK, collision avoidance",
+ "~20 ms",
+ ),
+ (
+ 3.5,
+ 2.6,
+ 7.4,
+ "ROBOT-LEVEL",
+ "Komendy ruchu",
+ GRAY1,
+ "RAPID, KRL, Karel\nPDL2, URScript, ROS",
+ "~100 ms",
+ ),
+ (
+ 5.0,
+ 3.4,
+ 6.6,
+ "TASK-LEVEL",
+ "Opis celu",
+ GRAY4,
+ "PDDL, BT, STRIPS\nplanowanie AI",
+ "~sekundy",
+ ),
+ ]
+
+ h = 1.3
+ for y, lx, rx, label, sublabel, fill, examples, timing in layers:
+ rx - lx
+ # Draw trapezoid
+ trap = plt.Polygon(
+ [(lx, y), (rx, y), (rx - 0.4, y + h), (lx + 0.4, y + h)],
+ closed=True,
+ facecolor=fill,
+ edgecolor=LN,
+ lw=1.5,
+ )
+ ax.add_patch(trap)
+
+ # Label
+ ax.text(
+ (lx + rx) / 2,
+ y + h * 0.65,
+ label,
+ ha="center",
+ va="center",
+ fontsize=9,
+ fontweight="bold",
+ )
+ ax.text(
+ (lx + rx) / 2,
+ y + h * 0.35,
+ sublabel,
+ ha="center",
+ va="center",
+ fontsize=7,
+ style="italic",
+ )
+
+ # Examples - right side
+ ax.text(
+ rx + 0.2,
+ y + h * 0.5,
+ examples,
+ ha="left",
+ va="center",
+ fontsize=6.5,
+ color="#333333",
+ )
+
+ # Timing - left side
+ ax.text(
+ lx - 0.2,
+ y + h * 0.5,
+ timing,
+ ha="right",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ color="#333333",
+ )
+
+ # Arrow on left
+ ax.annotate(
+ "",
+ xy=(0.5, 6.2),
+ xytext=(0.5, 0.8),
+ arrowprops={"arrowstyle": "->", "color": "black", "lw": 2},
+ )
+ ax.text(
+ 0.5,
+ 3.5,
+ "Abstrakcja\nrośnie",
+ ha="center",
+ va="center",
+ fontsize=7,
+ rotation=90,
+ fontweight="bold",
+ )
+
+ # Arrow on right side for timing
+ ax.annotate(
+ "",
+ xy=(9.7, 0.8),
+ xytext=(9.7, 6.2),
+ arrowprops={"arrowstyle": "->", "color": "black", "lw": 2},
+ )
+ ax.text(
+ 9.7,
+ 3.5,
+ "Szybkość\nreakcji",
+ ha="center",
+ va="center",
+ fontsize=7,
+ rotation=270,
+ fontweight="bold",
+ )
+
+ # Mnemonic at bottom
+ ax.text(
+ 5.0,
+ 0.0,
+ 'Mnemonik: „Tomek Robi Mechaniczne Serwa" (T→R→M→S, od góry do dołu)',
+ ha="center",
+ va="center",
+ fontsize=7,
+ style="italic",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": LN,
+ "lw": 0.8,
+ },
+ )
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "robot_trms_pyramid.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close(fig)
+ print(" ✓ robot_trms_pyramid.png")
+
+
+# ============================================================
+# 2. Vendor Languages Comparison
+# ============================================================
+def draw_vendor_comparison() -> None:
+ """Draw vendor comparison."""
+ fig, ax = plt.subplots(1, 1, figsize=(8.27, 5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 7.5)
+ ax.axis("off")
+ ax.set_title(
+ "Języki producentów robotów — porównanie",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Table headers
+ headers = [
+ "Cecha",
+ "RAPID\n(ABB)",
+ "KRL\n(KUKA)",
+ "Karel\n(FANUC)",
+ "PDL2\n(Comau)",
+ "URScript\n(UR)",
+ ]
+ col_widths = [1.8, 1.6, 1.6, 1.6, 1.6, 1.6]
+ col_x = [0.1]
+ for w in col_widths[:-1]:
+ col_x.append(col_x[-1] + w)
+
+ row_h = 0.7
+ header_y = 6.3
+ rows = [
+ [
+ "Składnia",
+ "typ własny\nstrukturalna",
+ "Pascal-like\nstrukturalna",
+ "Pascal-like\nstrukturalna",
+ "proceduralna\nC-like",
+ "Python-like\nskryptowy",
+ ],
+ [
+ "Ruch liniowy",
+ "MoveL",
+ "LIN",
+ "MOVE TO\nw/LINEAR",
+ "MOVE\nLINEAR TO",
+ "movel()",
+ ],
+ ["Ruch joint", "MoveJ", "PTP", "MOVE TO", "MOVE TO", "movej()"],
+ [
+ "Ruch kołowy",
+ "MoveC",
+ "CIRC",
+ "(brak\nwbudow.)",
+ "MOVE\nCIRCULAR",
+ "movec()",
+ ],
+ [
+ "I/O",
+ "SetDO/\nWaitDI",
+ "OUT/IN",
+ "DOUT/DIN",
+ "OUT/IN",
+ "set_digital\n_out()",
+ ],
+ [
+ "Zmienne",
+ "num, robtarget\nstring, bool",
+ "INT, REAL\nPOS, E6POS",
+ "INTEGER\nPOSITION",
+ "INTEGER\nPOSITION",
+ "int, float\npose",
+ ],
+ [
+ "Symulator",
+ "RobotStudio",
+ "KUKA.Sim",
+ "ROBOGUIDE",
+ "RoboSim",
+ "URSim\n(darmowy)",
+ ],
+ ]
+
+ # Draw header row
+ for j, (hdr, w) in enumerate(zip(headers, col_widths, strict=False)):
+ x = col_x[j]
+ fill = GRAY2 if j == 0 else GRAY1
+ draw_box(
+ ax,
+ x,
+ header_y,
+ w - 0.05,
+ row_h,
+ hdr,
+ fill=fill,
+ fontsize=7,
+ fontweight="bold",
+ rounded=False,
+ )
+
+ # Draw data rows
+ for i, row in enumerate(rows):
+ y = header_y - (i + 1) * row_h
+ for j, (cell, w) in enumerate(zip(row, col_widths, strict=False)):
+ x = col_x[j]
+ fill = GRAY4 if j == 0 else (WHITE if i % 2 == 0 else GRAY4)
+ fw = "bold" if j == 0 else "normal"
+ draw_box(
+ ax,
+ x,
+ y,
+ w - 0.05,
+ row_h - 0.02,
+ cell,
+ fill=fill,
+ fontsize=6,
+ fontweight=fw,
+ rounded=False,
+ )
+
+ # Note
+ ax.text(
+ 5.0,
+ 0.5,
+ "Vendor lock-in: program w RAPID ≠ działa na KUKA. "
+ "ROS/ROS 2 jako warstwa unifikująca.",
+ ha="center",
+ va="center",
+ fontsize=7,
+ style="italic",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": LN,
+ "lw": 0.8,
+ },
+ )
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "robot_vendor_comparison.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close(fig)
+ print(" ✓ robot_vendor_comparison.png")
+
+
+# ============================================================
+# 3. Robot Movement Types (PTP, LIN, CIRC)
+# ============================================================
+def draw_movement_types() -> None:
+ """Draw movement types."""
+ fig, axes = plt.subplots(1, 3, figsize=(8.27, 3.2))
+ fig.suptitle(
+ "Typy ruchu robota: PTP, LIN, CIRC",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=0.98,
+ )
+
+ # --- PTP (Point-to-Point) ---
+ ax = axes[0]
+ ax.set_xlim(-0.5, 4.5)
+ ax.set_ylim(-0.5, 4.5)
+ ax.set_aspect("equal")
+ ax.set_title("PTP (Point-to-Point)\nMoveJ / PTP", fontsize=8, fontweight="bold")
+ ax.grid(True, alpha=0.3)
+
+ # Start and end
+ start = (0.5, 0.5)
+ end = (3.5, 3.5)
+ ax.plot(*start, "ko", ms=10, zorder=5)
+ ax.plot(*end, "ks", ms=10, zorder=5)
+ ax.text(start[0] - 0.3, start[1] - 0.3, "Start", fontsize=7, ha="center")
+ ax.text(end[0] + 0.3, end[1] + 0.3, "Cel", fontsize=7, ha="center")
+
+ # Curved path (joint space = not necessarily straight in Cartesian)
+ t = np.linspace(0, 1, 50)
+ x_ptp = start[0] + (end[0] - start[0]) * t + 0.8 * np.sin(np.pi * t)
+ y_ptp = start[1] + (end[1] - start[1]) * t - 0.3 * np.sin(np.pi * t)
+ ax.plot(x_ptp, y_ptp, "k-", lw=2)
+ ax.annotate(
+ "",
+ xy=(x_ptp[-1], y_ptp[-1]),
+ xytext=(x_ptp[-3], y_ptp[-3]),
+ arrowprops={"arrowstyle": "->", "color": "black", "lw": 2},
+ )
+
+ ax.text(
+ 2.8,
+ 1.2,
+ "Ścieżka\nw kartezjańskiej\nnieokreślona!",
+ fontsize=6,
+ ha="center",
+ style="italic",
+ bbox={"boxstyle": "round", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+ ax.text(
+ 2.0,
+ -0.3,
+ "Najszybszy, ale\nścieżka nieprzewidywalna",
+ fontsize=6,
+ ha="center",
+ style="italic",
+ )
+ ax.set_xlabel("")
+ ax.set_ylabel("")
+ ax.tick_params(labelsize=6)
+
+ # --- LIN (Linear) ---
+ ax = axes[1]
+ ax.set_xlim(-0.5, 4.5)
+ ax.set_ylim(-0.5, 4.5)
+ ax.set_aspect("equal")
+ ax.set_title("LIN (Linear)\nMoveL / LIN", fontsize=8, fontweight="bold")
+ ax.grid(True, alpha=0.3)
+
+ start = (0.5, 1.0)
+ end = (3.5, 3.5)
+ ax.plot(*start, "ko", ms=10, zorder=5)
+ ax.plot(*end, "ks", ms=10, zorder=5)
+ ax.text(start[0] - 0.3, start[1] - 0.3, "Start", fontsize=7, ha="center")
+ ax.text(end[0] + 0.3, end[1] + 0.3, "Cel", fontsize=7, ha="center")
+
+ # Straight line
+ ax.plot([start[0], end[0]], [start[1], end[1]], "k-", lw=2)
+ ax.annotate(
+ "",
+ xy=end,
+ xytext=(
+ start[0] + 0.9 * (end[0] - start[0]),
+ start[1] + 0.9 * (end[1] - start[1]),
+ ),
+ arrowprops={"arrowstyle": "->", "color": "black", "lw": 2},
+ )
+
+ # Show intermediate points
+ for frac in [0.25, 0.5, 0.75]:
+ px = start[0] + frac * (end[0] - start[0])
+ py = start[1] + frac * (end[1] - start[1])
+ ax.plot(px, py, "k.", ms=6)
+
+ ax.text(
+ 2.0,
+ -0.3,
+ "Prosta linia TCP\nIK w każdym punkcie",
+ fontsize=6,
+ ha="center",
+ style="italic",
+ )
+ ax.tick_params(labelsize=6)
+
+ # --- CIRC (Circular) ---
+ ax = axes[2]
+ ax.set_xlim(-0.5, 4.5)
+ ax.set_ylim(-0.5, 4.5)
+ ax.set_aspect("equal")
+ ax.set_title("CIRC (Circular)\nMoveC / CIRC", fontsize=8, fontweight="bold")
+ ax.grid(True, alpha=0.3)
+
+ # Arc through 3 points
+ center = (2.0, 1.5)
+ r = 2.0
+ theta_start = np.radians(20)
+ theta_end = np.radians(160)
+ theta = np.linspace(theta_start, theta_end, 50)
+ x_circ = center[0] + r * np.cos(theta)
+ y_circ = center[1] + r * np.sin(theta)
+
+ ax.plot(x_circ, y_circ, "k-", lw=2)
+ ax.annotate(
+ "",
+ xy=(x_circ[-1], y_circ[-1]),
+ xytext=(x_circ[-3], y_circ[-3]),
+ arrowprops={"arrowstyle": "->", "color": "black", "lw": 2},
+ )
+
+ # Start, auxiliary, end points
+ ax.plot(x_circ[0], y_circ[0], "ko", ms=10, zorder=5)
+ ax.plot(x_circ[24], y_circ[24], "k^", ms=8, zorder=5)
+ ax.plot(x_circ[-1], y_circ[-1], "ks", ms=10, zorder=5)
+ ax.text(x_circ[0] + 0.3, y_circ[0] - 0.3, "Start", fontsize=7)
+ ax.text(
+ x_circ[24] + 0.05, y_circ[24] + 0.25, "Pkt\npomocniczy", fontsize=6, ha="center"
+ )
+ ax.text(x_circ[-1] - 0.5, y_circ[-1] - 0.3, "Cel", fontsize=7)
+
+ # Center
+ ax.plot(*center, "k+", ms=8, mew=1.5)
+ ax.text(center[0], center[1] - 0.3, "środek", fontsize=6, ha="center")
+
+ ax.text(
+ 2.0,
+ -0.3,
+ "Łuk wyznaczony\nprzez 3 punkty",
+ fontsize=6,
+ ha="center",
+ style="italic",
+ )
+ ax.tick_params(labelsize=6)
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "robot_movement_types.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close(fig)
+ print(" ✓ robot_movement_types.png")
+
+
+# ============================================================
+# 4. Online vs Offline Programming
+# ============================================================
+def draw_online_offline() -> None:
+ """Draw online offline."""
+ fig, ax = plt.subplots(1, 1, figsize=(8.27, 4.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 6.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Programowanie robotów: Online (teach-in) vs Offline",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # === ONLINE side (left) ===
+ # Title
+ draw_box(
+ ax,
+ 0.3,
+ 5.2,
+ 4.2,
+ 0.8,
+ "ONLINE\n(teach-in / pendant)",
+ fill=GRAY2,
+ fontsize=9,
+ fontweight="bold",
+ )
+
+ steps_online = [
+ (4.2, "Operator przy robocie\nz teach pendantem"),
+ (3.2, 'Prowadzi ramię\n„za rękę" do punktów'),
+ (2.2, "Robot zapamiętuje\npozycje (record)"),
+ (1.2, "Odtwarzanie\nzapisanej ścieżki"),
+ ]
+ for y, txt in steps_online:
+ draw_box(ax, 0.5, y, 3.8, 0.8, txt, fill=WHITE, fontsize=7)
+
+ for i in range(len(steps_online) - 1):
+ draw_arrow(ax, 2.4, steps_online[i][0], 2.4, steps_online[i + 1][0] + 0.8)
+
+ # Pros/cons
+ ax.text(
+ 2.4,
+ 0.6,
+ "✓ Proste, intuicyjne\n✗ Wymaga zatrzymania produkcji\n✗ Niska precyzja",
+ ha="center",
+ va="center",
+ fontsize=6.5,
+ bbox={"boxstyle": "round", "facecolor": GRAY4, "edgecolor": GRAY5, "lw": 0.8},
+ )
+
+ # Divider
+ ax.plot([4.9, 4.9], [0.3, 6.2], "k--", lw=1, alpha=0.5)
+
+ # === OFFLINE side (right) ===
+ draw_box(
+ ax,
+ 5.3,
+ 5.2,
+ 4.2,
+ 0.8,
+ "OFFLINE\n(symulacja / CAD/CAM)",
+ fill=GRAY2,
+ fontsize=9,
+ fontweight="bold",
+ )
+
+ steps_offline = [
+ (4.2, "Model 3D robota +\nśrodowisko w symulatorze"),
+ (3.2, "Programowanie ścieżek\nw środowisku wirtualnym"),
+ (2.2, "Weryfikacja kolizji\ni optymalizacja"),
+ (1.2, "Transfer na\nrzeczywistego robota"),
+ ]
+ for y, txt in steps_offline:
+ draw_box(ax, 5.5, y, 3.8, 0.8, txt, fill=WHITE, fontsize=7)
+
+ for i in range(len(steps_offline) - 1):
+ draw_arrow(ax, 7.4, steps_offline[i][0], 7.4, steps_offline[i + 1][0] + 0.8)
+
+ ax.text(
+ 7.4,
+ 0.6,
+ "✓ Bez zatrzymania produkcji\n✓ Wysoka precyzja, symulacja\n✗ Wymaga kalibracji",
+ ha="center",
+ va="center",
+ fontsize=6.5,
+ bbox={"boxstyle": "round", "facecolor": GRAY4, "edgecolor": GRAY5, "lw": 0.8},
+ )
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "robot_online_offline.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close(fig)
+ print(" ✓ robot_online_offline.png")
+
+
+# ============================================================
+# 5. ROS Architecture (pub/sub)
+# ============================================================
+def draw_ros_architecture() -> None:
+ """Draw ros architecture."""
+ fig, ax = plt.subplots(1, 1, figsize=(8.27, 4.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 6.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "ROS — architektura publish/subscribe",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Nodes
+ nodes = [
+ (1.0, 4.5, "Czujnik\n(LiDAR)", GRAY1),
+ (1.0, 2.5, "Kamera\n(RGB-D)", GRAY1),
+ (4.0, 4.5, "Lokalizacja\n(SLAM)", GRAY4),
+ (4.0, 2.5, "Percepcja\n(detekcja)", GRAY4),
+ (7.0, 3.5, "Planowanie\nruchu (MoveIt)", GRAY2),
+ (7.0, 1.0, "Sterownik\nsilników", GRAY3),
+ ]
+
+ for x, y, txt, fill in nodes:
+ draw_box(ax, x, y, 2.2, 1.0, txt, fill=fill, fontsize=7, fontweight="bold")
+
+ # Topics (arrows with labels)
+ topics = [
+ # (from_x, from_y, to_x, to_y, label)
+ (3.2, 5.0, 4.0, 5.0, "/scan"),
+ (3.2, 3.0, 4.0, 3.0, "/image"),
+ (6.2, 5.0, 7.0, 4.3, "/pose"),
+ (6.2, 3.0, 7.0, 3.8, "/objects"),
+ (8.0, 3.5, 8.0, 2.0, "/cmd_vel"),
+ ]
+
+ for x1, y1, x2, y2, label in topics:
+ draw_arrow(ax, x1, y1, x2, y2, lw=1.5)
+ mx, my = (x1 + x2) / 2, (y1 + y2) / 2
+ ax.text(
+ mx,
+ my + 0.2,
+ label,
+ ha="center",
+ va="bottom",
+ fontsize=6,
+ fontweight="bold",
+ style="italic",
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": WHITE,
+ "edgecolor": GRAY5,
+ "lw": 0.5,
+ },
+ )
+
+ # ROS Master / roscore
+ draw_box(
+ ax,
+ 3.5,
+ 0.3,
+ 3.0,
+ 0.8,
+ "ROS Master (roscore)\nRejestr węzłów i tematów",
+ fill=GRAY2,
+ fontsize=7,
+ fontweight="bold",
+ )
+
+ # Dashed lines to master
+ for x, y, _, _ in nodes[:4]:
+ ax.plot([x + 1.1, 5.0], [y, 1.1], "k:", lw=0.5, alpha=0.4)
+
+ # Legend
+ ax.text(
+ 0.3,
+ 0.8,
+ "Węzeł (Node) = proces\n"
+ "Temat (Topic) = kanał pub/sub\n"
+ "Wiadomość = typowany komunikat",
+ ha="left",
+ va="center",
+ fontsize=6,
+ bbox={"boxstyle": "round", "facecolor": GRAY4, "edgecolor": LN, "lw": 0.8},
+ )
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "robot_ros_architecture.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close(fig)
+ print(" ✓ robot_ros_architecture.png")
+
+
+# ============================================================
+# 6. RAPID program structure example
+# ============================================================
+def draw_rapid_structure() -> None:
+ """Draw rapid structure."""
+ fig, ax = plt.subplots(1, 1, figsize=(8.27, 5.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 8)
+ ax.axis("off")
+ ax.set_title(
+ "Struktura programu RAPID (ABB) — przykład pick & place",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Program structure blocks
+
+ # Simplified: just draw code blocks
+ code_sections = [
+ (
+ "Deklaracje danych (stałe, zmienne)",
+ GRAY4,
+ [
+ "CONST robtarget pHome := [[500,0,600],[1,0,0,0],...];",
+ "CONST robtarget pPick := [[400,200,100],[1,0,0,0],...];",
+ "CONST robtarget pPlace := [[400,-200,100],[1,0,0,0],...];",
+ "VAR num nCycles := 0;",
+ "PERS tooldata tGripper := [...];",
+ ],
+ ),
+ (
+ "Procedura główna: main()",
+ GRAY1,
+ [
+ "PROC main()",
+ " MoveJ pHome, v1000, z50, tGripper;",
+ " WHILE TRUE DO",
+ " PickPart;",
+ " PlacePart;",
+ " Incr nCycles;",
+ " ENDWHILE",
+ "ENDPROC",
+ ],
+ ),
+ (
+ "Podprocedura: PickPart()",
+ GRAY1,
+ [
+ "PROC PickPart()",
+ " MoveL Offs(pPick,0,0,50), v500, z10, tGripper;",
+ " MoveL pPick, v100, fine, tGripper;",
+ " SetDO doGripper, 1; ! zamknij chwytak",
+ " WaitTime 0.5;",
+ " MoveL Offs(pPick,0,0,50), v500, z10, tGripper;",
+ "ENDPROC",
+ ],
+ ),
+ ]
+
+ y_cur = 7.2
+ for title, fill, lines in code_sections:
+ 0.25 * len(lines) + 0.5
+ # Title bar
+ draw_box(
+ ax,
+ 0.5,
+ y_cur - 0.35,
+ 9.0,
+ 0.35,
+ title,
+ fill=fill,
+ fontsize=7,
+ fontweight="bold",
+ rounded=False,
+ )
+ y_cur -= 0.35
+
+ # Code lines
+ for _i, line in enumerate(lines):
+ y_cur -= 0.25
+ ax.text(
+ 0.7,
+ y_cur + 0.12,
+ line,
+ fontsize=5.5,
+ fontfamily="monospace",
+ va="center",
+ )
+
+ # Border around code
+ code_h = 0.25 * len(lines)
+ rect = mpatches.Rectangle(
+ (0.5, y_cur - 0.05),
+ 9.0,
+ code_h + 0.15,
+ lw=0.8,
+ edgecolor=GRAY5,
+ facecolor=WHITE,
+ zorder=-1,
+ )
+ ax.add_patch(rect)
+
+ y_cur -= 0.3
+
+ # Annotations on right
+ annotations = [
+ (
+ 6.5,
+ "robtarget = pozycja\nkartezjańska + orientacja\n+ konfiguracja ramienia",
+ ),
+ (
+ 4.5,
+ "v500 = prędkość 500 mm/s\nz10 = strefa zbliżenia 10mm\nfine = dokładne dojście",
+ ),
+ (2.5, "SetDO = Digital Output\nSterowanie I/O\n(chwytak, zawory)"),
+ ]
+
+ for yy, txt in annotations:
+ ax.text(
+ 9.8,
+ yy,
+ txt,
+ fontsize=5.5,
+ ha="left",
+ va="center",
+ bbox={
+ "boxstyle": "round,pad=0.2",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY5,
+ "lw": 0.5,
+ },
+ )
+
+ fig.tight_layout()
+ fig.savefig(
+ str(Path(OUTPUT_DIR) / "robot_rapid_example.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close(fig)
+ print(" ✓ robot_rapid_example.png")
+
+
+# ============================================================
+# Main
+# ============================================================
+if __name__ == "__main__":
+ print("Generating PYTANIE 16 diagrams...")
+ draw_trms_pyramid()
+ draw_vendor_comparison()
+ draw_movement_types()
+ draw_online_offline()
+ draw_ros_architecture()
+ draw_rapid_structure()
+ print("Done! All diagrams saved to", OUTPUT_DIR)
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_scheduling_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_scheduling_diagrams.py
new file mode 100755
index 0000000..f9c8ba0
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_scheduling_diagrams.py
@@ -0,0 +1,1418 @@
+#!/usr/bin/env python3
+"""Generate diagrams for PYTANIE 17: Szeregowanie zadań (Scheduling).
+
+Diagrams:
+ 1. Graham notation \u03b1|β|\u03b3 visual mnemonic map
+ 2. Johnson's algorithm Gantt chart (F2||Cmax example)
+ 3. SPT vs LPT comparison Gantt (1||ΣCⱼ)
+ 4. Flow shop vs Job shop visual comparison
+ 5. Scheduling complexity landscape
+
+All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 11
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.2,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=True,
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y), w, h, boxstyle="round,pad=0.05", lw=lw, edgecolor=LN, facecolor=fill
+ )
+ else:
+ rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ )
+
+
+# ============================================================
+# 1. GRAHAM NOTATION alpha|β|gamma — MNEMONIC MAP
+# ============================================================
+def draw_graham_notation() -> None:
+ """Draw graham notation."""
+ _fig, ax = plt.subplots(1, 1, figsize=(8.27, 10))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 14)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Notacja Grahama \u03b1 | β | \u03b3 — Mapa mnemoniczna",
+ fontsize=FS_TITLE + 1,
+ fontweight="bold",
+ pad=12,
+ )
+
+ # === TOP: The three fields ===
+ # Big formula bar
+ bar_y = 12.5
+ bar_h = 1.0
+ # alpha box
+ rect = FancyBboxPatch(
+ (0.5, bar_y),
+ 2.5,
+ bar_h,
+ boxstyle="round,pad=0.08",
+ lw=2,
+ edgecolor=LN,
+ facecolor=GRAY1,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 1.75,
+ bar_y + bar_h / 2,
+ "\u03b1",
+ fontsize=20,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ )
+ ax.text(
+ 1.75,
+ bar_y - 0.25,
+ "MASZYNY",
+ fontsize=8,
+ fontweight="bold",
+ ha="center",
+ va="top",
+ color="#444444",
+ )
+
+ # separator |
+ ax.text(
+ 3.3,
+ bar_y + bar_h / 2,
+ "|",
+ fontsize=24,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ )
+
+ # β box
+ rect = FancyBboxPatch(
+ (3.7, bar_y),
+ 2.5,
+ bar_h,
+ boxstyle="round,pad=0.08",
+ lw=2,
+ edgecolor=LN,
+ facecolor=GRAY2,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 4.95,
+ bar_y + bar_h / 2,
+ "β",
+ fontsize=20,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ )
+ ax.text(
+ 4.95,
+ bar_y - 0.25,
+ "OGRANICZENIA",
+ fontsize=8,
+ fontweight="bold",
+ ha="center",
+ va="top",
+ color="#444444",
+ )
+
+ # separator |
+ ax.text(
+ 6.5,
+ bar_y + bar_h / 2,
+ "|",
+ fontsize=24,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ )
+
+ # gamma box
+ rect = FancyBboxPatch(
+ (6.9, bar_y),
+ 2.5,
+ bar_h,
+ boxstyle="round,pad=0.08",
+ lw=2,
+ edgecolor=LN,
+ facecolor=GRAY3,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 8.15,
+ bar_y + bar_h / 2,
+ "\u03b3",
+ fontsize=20,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ )
+ ax.text(
+ 8.15,
+ bar_y - 0.25,
+ "CEL",
+ fontsize=8,
+ fontweight="bold",
+ ha="center",
+ va="top",
+ color="#444444",
+ )
+
+ # === SECTION alpha: MACHINES ===
+ sec_y = 11.5
+ ax.text(
+ 0.3,
+ sec_y,
+ '\u03b1 — „1 Prawdziwy Quasi-Rycerz Forsuje Jaskinię Orków"',
+ fontsize=8,
+ fontweight="bold",
+ va="top",
+ style="italic",
+ color="#333333",
+ )
+
+ alpha_items = [
+ ("1", "jedna maszyna", "●", GRAY4),
+ ("Pm", "identyczne Parallel", "●●●", GRAY1),
+ ("Qm", "Quasi-uniform\n(różne prędkości)", "●●◐", GRAY4),
+ ("Rm", "Random unrelated\n(czasy per para)", "●◆▲", GRAY1),
+ ("Fm", "Flow shop\n(ta sama kolejność)", "→→→", GRAY2),
+ ("Jm", "Job shop\n(indyw. trasy)", "↗↙↘", GRAY4),
+ ("Om", "Open shop\n(dowolna kolej.)", "?→?", GRAY1),
+ ]
+
+ col_w = 1.28
+ box_h_a = 1.1
+ start_x = 0.3
+ start_y = 9.6
+
+ for i, (symbol, desc, icon, fill) in enumerate(alpha_items):
+ x = start_x + i * col_w
+ y = start_y
+ rect = FancyBboxPatch(
+ (x, y),
+ col_w - 0.1,
+ box_h_a,
+ boxstyle="round,pad=0.04",
+ lw=1,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + (col_w - 0.1) / 2,
+ y + box_h_a - 0.15,
+ symbol,
+ ha="center",
+ va="top",
+ fontsize=9,
+ fontweight="bold",
+ )
+ ax.text(
+ x + (col_w - 0.1) / 2,
+ y + box_h_a / 2 - 0.1,
+ desc,
+ ha="center",
+ va="center",
+ fontsize=5.5,
+ )
+ ax.text(
+ x + (col_w - 0.1) / 2, y + 0.12, icon, ha="center", va="bottom", fontsize=7
+ )
+
+ # Complexity arrow under alpha
+ arr_y = 9.35
+ ax.annotate(
+ "",
+ xy=(9.0, arr_y),
+ xytext=(0.5, arr_y),
+ arrowprops={"arrowstyle": "->", "color": "#666666", "lw": 1.5},
+ )
+ ax.text(
+ 4.8,
+ arr_y - 0.18,
+ "rosnąca złożoność →",
+ ha="center",
+ fontsize=6,
+ color="#666666",
+ )
+
+ # === SECTION β: CONSTRAINTS ===
+ sec_y2 = 8.9
+ ax.text(
+ 0.3,
+ sec_y2,
+ "β — „Robak Daje Deadline: Przerwy Poprzedzają Pojedyncze Setup'y\"",
+ fontsize=8,
+ fontweight="bold",
+ va="top",
+ style="italic",
+ color="#333333",
+ )
+
+ beta_items = [
+ ("rⱼ", "release\ndates", "Robak\ndostępne\nod czasu rⱼ", GRAY1),
+ ("dⱼ", "due\ndates", "Daje\ntermin soft\n(kara za spóźn.)", GRAY4),
+ ("d̄ⱼ", "dead-\nlines", "Deadline\ntermin hard\n(musi dotrzymać)", GRAY1),
+ ("pmtn", "preemp-\ntion", "Przerwy\nmożna\nprzerwać", GRAY2),
+ ("prec", "prece-\ndencje", "Poprzedzają\nA->B (DAG)", GRAY4),
+ ("pⱼ=1", "unit\ntime", "Pojedyncze\nwszystkie = 1", GRAY1),
+ ("sⱼₖ", "setup\ntimes", "Setup'y\nprzezbrojenie\nmiędzy j->k", GRAY4),
+ ]
+
+ start_y2 = 7.0
+ box_h_b = 1.4
+
+ for i, (symbol, _label, desc, fill) in enumerate(beta_items):
+ x = start_x + i * col_w
+ y = start_y2
+ rect = FancyBboxPatch(
+ (x, y),
+ col_w - 0.1,
+ box_h_b,
+ boxstyle="round,pad=0.04",
+ lw=1,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + (col_w - 0.1) / 2,
+ y + box_h_b - 0.12,
+ symbol,
+ ha="center",
+ va="top",
+ fontsize=9,
+ fontweight="bold",
+ )
+ ax.text(
+ x + (col_w - 0.1) / 2,
+ y + box_h_b / 2 - 0.05,
+ desc,
+ ha="center",
+ va="center",
+ fontsize=5,
+ )
+
+ # === SECTION gamma: CRITERIA ===
+ sec_y3 = 6.5
+ ax.text(
+ 0.3,
+ sec_y3,
+ '\u03b3 — „Ciężki Sum Ważony Lata, Tardiness Uderza"',
+ fontsize=8,
+ fontweight="bold",
+ va="top",
+ style="italic",
+ color="#333333",
+ )
+
+ gamma_items = [
+ ("Cmax", "makespan\nmax(Cⱼ)", "Jak długo\ntrwa WSZYSTKO?", GRAY2),
+ ("ΣCⱼ", "suma\nukończeń", "Średni czas\noczekiwania?", GRAY4),
+ ("ΣwⱼCⱼ", "ważona\nsuma", "Priorytety\nzadań?", GRAY1),
+ ("Lmax", "max\nopóźnienie", "Najgorsze\nspóźnienie?", GRAY2),
+ ("ΣTⱼ", "suma\nspóźnień", "Łączne\nspóźnienia?", GRAY4),
+ ("ΣUⱼ", "liczba\nspóźnionych", "Ile spóźnionych\nzadań?", GRAY1),
+ ]
+
+ start_y3 = 4.5
+ box_h_g = 1.4
+ col_w_g = 1.5
+
+ for i, (symbol, label, question, fill) in enumerate(gamma_items):
+ x = start_x + i * col_w_g
+ y = start_y3
+ rect = FancyBboxPatch(
+ (x, y),
+ col_w_g - 0.1,
+ box_h_g,
+ boxstyle="round,pad=0.04",
+ lw=1,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + (col_w_g - 0.1) / 2,
+ y + box_h_g - 0.1,
+ symbol,
+ ha="center",
+ va="top",
+ fontsize=9,
+ fontweight="bold",
+ )
+ ax.text(
+ x + (col_w_g - 0.1) / 2,
+ y + box_h_g / 2 - 0.05,
+ label,
+ ha="center",
+ va="center",
+ fontsize=6,
+ )
+ ax.text(
+ x + (col_w_g - 0.1) / 2,
+ y + 0.15,
+ f'„{question}"',
+ ha="center",
+ va="bottom",
+ fontsize=5,
+ style="italic",
+ )
+
+ # === BOTTOM: Example + Optimal methods ===
+ ex_y = 3.5
+ ax.text(
+ 0.3,
+ ex_y,
+ "Przykłady zapisu i optymalne metody:",
+ fontsize=8,
+ fontweight="bold",
+ va="top",
+ )
+
+ examples = [
+ ("1 || ΣCⱼ", "SPT (najkrótsze\nnajpierw)", "O(n log n)", GRAY1),
+ ("1 || Lmax", "EDD (najwcześniejszy\ntermin)", "O(n log n)", GRAY4),
+ ("F2 || Cmax", "Algorytm\nJohnsona", "O(n log n)", GRAY2),
+ ("Pm || Cmax", "LPT heurystyka\n(NP-trudny!)", "NP-hard", GRAY3),
+ ("Jm || Cmax", "Branch & Bound\n(NP-trudny!)", "NP-hard", GRAY5),
+ ]
+
+ ex_start_y = 1.8
+ ex_box_w = 1.72
+ ex_box_h = 1.4
+
+ for i, (notation, method, complexity, fill) in enumerate(examples):
+ x = start_x + i * (ex_box_w + 0.1)
+ y = ex_start_y
+ rect = FancyBboxPatch(
+ (x, y),
+ ex_box_w,
+ ex_box_h,
+ boxstyle="round,pad=0.04",
+ lw=1.2,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + ex_box_w / 2,
+ y + ex_box_h - 0.12,
+ notation,
+ ha="center",
+ va="top",
+ fontsize=8,
+ fontweight="bold",
+ fontfamily="monospace",
+ )
+ ax.text(
+ x + ex_box_w / 2,
+ y + ex_box_h / 2 - 0.05,
+ method,
+ ha="center",
+ va="center",
+ fontsize=6,
+ )
+ ax.text(
+ x + ex_box_w / 2,
+ y + 0.12,
+ complexity,
+ ha="center",
+ va="bottom",
+ fontsize=6.5,
+ fontweight="bold",
+ color="#555555",
+ )
+
+ # Footer mnemonic summary
+ ax.text(
+ 5.0,
+ 0.8,
+ '„\u03b1|β|\u03b3 = Maszyny | Ograniczenia | Cel"',
+ ha="center",
+ fontsize=9,
+ fontweight="bold",
+ style="italic",
+ color="#333333",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ "lw": 1,
+ },
+ )
+
+ ax.text(
+ 5.0,
+ 0.2,
+ "\u03b1: ILE maszyn i JAKIE? β: JAKIE ograniczenia zadań? \u03b3: CO minimalizujemy?",
+ ha="center",
+ fontsize=7,
+ color="#555555",
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "scheduling_graham_notation.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ scheduling_graham_notation.png")
+
+
+# ============================================================
+# 2. JOHNSON'S ALGORITHM GANTT CHART
+# ============================================================
+def draw_johnson_gantt() -> None:
+ """Draw johnson gantt."""
+ _fig, axes = plt.subplots(
+ 2, 1, figsize=(8.27, 7), gridspec_kw={"height_ratios": [1, 1.8]}
+ )
+
+ # --- Top: The decision process ---
+ ax = axes[0]
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Algorytm Johnsona (F2 || Cmax) — Decyzja + Diagram Gantta",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Task table
+ tasks = ["J1", "J2", "J3", "J4", "J5"]
+ a_times = [4, 2, 6, 1, 3]
+ b_times = [5, 3, 2, 7, 4]
+ min_vals = [min(a, b) for a, b in zip(a_times, b_times, strict=False)]
+ min_on = ["M1" if a <= b else "M2" for a, b in zip(a_times, b_times, strict=False)]
+ assign = ["POCZątek" if m == "M1" else "KONIEC" for m in min_on]
+
+ # Draw table
+ col_w_t = 1.3
+ row_h = 0.55
+ headers = ["Zadanie", "aⱼ (M1)", "bⱼ (M2)", "min", "min na", "Przydziel"]
+ table_x = 0.8
+ table_y = 3.8
+
+ for j, hdr in enumerate(headers):
+ x = table_x + j * col_w_t
+ rect = mpatches.Rectangle(
+ (x, table_y), col_w_t, row_h, lw=1, edgecolor=LN, facecolor=GRAY2
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + col_w_t / 2,
+ table_y + row_h / 2,
+ hdr,
+ ha="center",
+ va="center",
+ fontsize=6.5,
+ fontweight="bold",
+ )
+
+ for i in range(5):
+ row_data = [
+ tasks[i],
+ str(a_times[i]),
+ str(b_times[i]),
+ str(min_vals[i]),
+ min_on[i],
+ assign[i],
+ ]
+ for j, val in enumerate(row_data):
+ x = table_x + j * col_w_t
+ y = table_y - (i + 1) * row_h
+ fill_c = GRAY1 if min_on[i] == "M1" else GRAY4
+ if j == 3: # min column - highlight
+ fill_c = GRAY3
+ rect = mpatches.Rectangle(
+ (x, y), col_w_t, row_h, lw=0.8, edgecolor=LN, facecolor=fill_c
+ )
+ ax.add_patch(rect)
+ fw = "bold" if j >= 3 else "normal"
+ ax.text(
+ x + col_w_t / 2,
+ y + row_h / 2,
+ val,
+ ha="center",
+ va="center",
+ fontsize=6.5,
+ fontweight=fw,
+ )
+
+ # Sorting result
+ result_y = 0.7
+ ax.text(
+ 5.0,
+ result_y + 0.4,
+ "Sortuj → POCZĄTEK ↑aⱼ: J4(1), J2(2), J5(3), J1(4) | KONIEC ↓bⱼ: J3(2)",
+ ha="center",
+ fontsize=7,
+ color="#333333",
+ )
+ ax.text(
+ 5.0,
+ result_y,
+ "Optymalna kolejność: J4 → J2 → J5 → J1 → J3",
+ ha="center",
+ fontsize=9,
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.2",
+ "facecolor": GRAY1,
+ "edgecolor": LN,
+ "lw": 1.2,
+ },
+ )
+
+ # --- Bottom: Gantt chart ---
+ ax2 = axes[1]
+ ax2.set_xlim(-1, 24)
+ ax2.set_ylim(-1, 4)
+ ax2.axis("off")
+
+ # Machines labels
+ m1_y = 2.5
+ m2_y = 0.8
+ bar_h = 0.9
+
+ ax2.text(
+ -0.8,
+ m1_y + bar_h / 2,
+ "M1",
+ ha="center",
+ va="center",
+ fontsize=11,
+ fontweight="bold",
+ )
+ ax2.text(
+ -0.8,
+ m2_y + bar_h / 2,
+ "M2",
+ ha="center",
+ va="center",
+ fontsize=11,
+ fontweight="bold",
+ )
+
+ # Schedule: J4 → J2 → J5 → J1 → J3
+ order = ["J4", "J2", "J5", "J1", "J3"]
+ a_ord = [1, 2, 3, 4, 6] # M1 times in order
+ b_ord = [7, 3, 4, 5, 2] # M2 times in order
+ fills = [GRAY1, GRAY2, GRAY4, GRAY3, GRAY5]
+ hatches = ["", "///", "", "\\\\\\", "xxx"]
+
+ # M1 schedule
+ m1_starts = []
+ t = 0
+ for a in a_ord:
+ m1_starts.append(t)
+ t += a
+ m1_ends = [s + a for s, a in zip(m1_starts, a_ord, strict=False)]
+
+ # M2 schedule (must wait for M1 finish AND previous M2 finish)
+ m2_starts = []
+ m2_ends = []
+ prev_m2_end = 0
+ for i, b in enumerate(b_ord):
+ start = max(m1_ends[i], prev_m2_end)
+ m2_starts.append(start)
+ m2_ends.append(start + b)
+ prev_m2_end = start + b
+
+ # Draw M1 bars
+ for i in range(5):
+ rect = mpatches.Rectangle(
+ (m1_starts[i], m1_y),
+ a_ord[i],
+ bar_h,
+ lw=1.2,
+ edgecolor=LN,
+ facecolor=fills[i],
+ hatch=hatches[i],
+ )
+ ax2.add_patch(rect)
+ ax2.text(
+ m1_starts[i] + a_ord[i] / 2,
+ m1_y + bar_h / 2,
+ f"{order[i]}\n({a_ord[i]})",
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ )
+
+ # Draw M2 bars
+ for i in range(5):
+ rect = mpatches.Rectangle(
+ (m2_starts[i], m2_y),
+ b_ord[i],
+ bar_h,
+ lw=1.2,
+ edgecolor=LN,
+ facecolor=fills[i],
+ hatch=hatches[i],
+ )
+ ax2.add_patch(rect)
+ ax2.text(
+ m2_starts[i] + b_ord[i] / 2,
+ m2_y + bar_h / 2,
+ f"{order[i]}\n({b_ord[i]})",
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ )
+
+ # Draw idle regions on M2
+ idle_starts = [0]
+ idle_ends = [m2_starts[0]]
+ for i in range(1, 5):
+ if m2_starts[i] > m2_ends[i - 1]:
+ idle_starts.append(m2_ends[i - 1])
+ idle_ends.append(m2_starts[i])
+
+ for s, e in zip(idle_starts, idle_ends, strict=False):
+ if e > s:
+ rect = mpatches.Rectangle(
+ (s, m2_y),
+ e - s,
+ bar_h,
+ lw=0.5,
+ edgecolor="#AAAAAA",
+ facecolor="white",
+ linestyle="--",
+ )
+ ax2.add_patch(rect)
+ ax2.text(
+ s + (e - s) / 2,
+ m2_y + bar_h / 2,
+ "idle",
+ ha="center",
+ va="center",
+ fontsize=5,
+ color="#999999",
+ )
+
+ # Time axis
+ ax_y = m2_y - 0.15
+ ax2.plot([0, 23], [ax_y, ax_y], color=LN, lw=0.8)
+ for t in range(0, 24, 2):
+ ax2.plot([t, t], [ax_y - 0.08, ax_y + 0.08], color=LN, lw=0.8)
+ ax2.text(t, ax_y - 0.25, str(t), ha="center", va="top", fontsize=6)
+ ax2.text(11.5, ax_y - 0.55, "czas", ha="center", fontsize=7)
+
+ # Cmax annotation
+ ax2.annotate(
+ f"Cmax = {m2_ends[-1]}",
+ xy=(m2_ends[-1], m2_y + bar_h),
+ xytext=(m2_ends[-1] + 0.5, m2_y + bar_h + 0.6),
+ fontsize=10,
+ fontweight="bold",
+ color="#333333",
+ arrowprops={"arrowstyle": "->", "color": "#333333", "lw": 1.5},
+ )
+
+ # Mnemonic at bottom
+ ax2.text(
+ 11,
+ -0.7,
+ '„Krótki na M1 → START (szybko karmi M2) Krótki na M2 → KONIEC (szybko kończy)"',
+ ha="center",
+ fontsize=7.5,
+ fontweight="bold",
+ style="italic",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ "lw": 0.8,
+ },
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "scheduling_johnson_gantt.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ scheduling_johnson_gantt.png")
+
+
+# ============================================================
+# 3. SPT vs LPT COMPARISON (1 || ΣCⱼ)
+# ============================================================
+def draw_spt_comparison() -> None:
+ """Draw spt comparison."""
+ fig, axes = plt.subplots(2, 1, figsize=(8.27, 5.5))
+
+ tasks_orig = [("J1", 5), ("J2", 3), ("J3", 8), ("J4", 2), ("J5", 6)]
+
+ spt_order = sorted(tasks_orig, key=lambda x: x[1])
+ lpt_order = sorted(tasks_orig, key=lambda x: -x[1])
+
+ fills_map = {"J1": GRAY1, "J2": GRAY2, "J3": GRAY3, "J4": GRAY4, "J5": GRAY5}
+ hatch_map = {"J1": "", "J2": "///", "J3": "xxx", "J4": "", "J5": "\\\\\\"}
+
+ for _idx, (ax, order_list, title, is_optimal) in enumerate(
+ [
+ (axes[0], spt_order, "SPT (Shortest Processing Time) — OPTYMALNE", True),
+ (axes[1], lpt_order, "LPT (Longest Processing Time) — gorsze!", False),
+ ]
+ ):
+ ax.set_xlim(-2, 26)
+ ax.set_ylim(-0.5, 2.5)
+ ax.axis("off")
+ color = "#222222" if is_optimal else "#666666"
+ marker = "✓" if is_optimal else "✗"
+ ax.set_title(
+ f"{marker} {title}",
+ fontsize=9,
+ fontweight="bold",
+ loc="left",
+ color=color,
+ pad=5,
+ )
+
+ bar_y = 1.0
+ bar_h = 0.8
+ t = 0
+ completions = []
+
+ for name, duration in order_list:
+ rect = mpatches.Rectangle(
+ (t, bar_y),
+ duration,
+ bar_h,
+ lw=1.2,
+ edgecolor=LN,
+ facecolor=fills_map[name],
+ hatch=hatch_map[name],
+ )
+ ax.add_patch(rect)
+ ax.text(
+ t + duration / 2,
+ bar_y + bar_h / 2,
+ f"{name}\n({duration})",
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ )
+ t += duration
+ completions.append(t)
+
+ # Completion time marker
+ ax.plot([t, t], [bar_y - 0.15, bar_y], color=LN, lw=0.8)
+ ax.text(
+ t,
+ bar_y - 0.25,
+ f"C={t}",
+ ha="center",
+ va="top",
+ fontsize=6,
+ color="#555555",
+ )
+
+ total = sum(completions)
+ # Time axis
+ ax.plot([0, 25], [bar_y - 0.05, bar_y - 0.05], color=LN, lw=0.5)
+
+ # Sum annotation
+ comp_str = " + ".join(str(c) for c in completions)
+ ax.text(
+ 25,
+ bar_y + bar_h / 2,
+ f"ΣCⱼ = {comp_str}\n = {total}",
+ ha="left",
+ va="center",
+ fontsize=7,
+ fontweight="bold" if is_optimal else "normal",
+ color=color,
+ bbox={
+ "boxstyle": "round,pad=0.2",
+ "facecolor": GRAY1 if is_optimal else "white",
+ "edgecolor": color,
+ "lw": 1,
+ },
+ )
+
+ # Bottom annotation
+ fig.text(
+ 0.5,
+ 0.02,
+ '„Short People To the front" — krótkie najpierw, jak niskie osoby w zdjęciu klasowym',
+ ha="center",
+ fontsize=8,
+ fontweight="bold",
+ style="italic",
+ color="#444444",
+ )
+
+ plt.tight_layout(rect=[0, 0.05, 1, 1])
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "scheduling_spt_comparison.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ scheduling_spt_comparison.png")
+
+
+# ============================================================
+# 4. FLOW SHOP vs JOB SHOP
+# ============================================================
+def draw_flow_vs_job() -> None:
+ """Draw flow vs job."""
+ _fig, axes = plt.subplots(1, 2, figsize=(8.27, 4.5))
+
+ # --- LEFT: Flow Shop ---
+ ax = axes[0]
+ ax.set_xlim(0, 6)
+ ax.set_ylim(0, 6)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Flow Shop (Fm)", fontsize=10, fontweight="bold", pad=8)
+
+ # Machines in a row
+ machines_x = [1, 3, 5]
+ machines_y = 3
+ mach_r = 0.4
+
+ for i, mx in enumerate(machines_x):
+ circle = plt.Circle(
+ (mx, machines_y), mach_r, facecolor=GRAY2, edgecolor=LN, lw=1.5
+ )
+ ax.add_patch(circle)
+ ax.text(
+ mx,
+ machines_y,
+ f"M{i + 1}",
+ ha="center",
+ va="center",
+ fontsize=9,
+ fontweight="bold",
+ )
+
+ # Arrows between machines
+ for i in range(len(machines_x) - 1):
+ draw_arrow(
+ ax,
+ machines_x[i] + mach_r + 0.05,
+ machines_y,
+ machines_x[i + 1] - mach_r - 0.05,
+ machines_y,
+ lw=2,
+ )
+
+ # Jobs all flowing the same way
+ jobs_flow = ["J1", "J2", "J3"]
+ for _j, (job, y_off) in enumerate(zip(jobs_flow, [0.8, 0, -0.8], strict=False)):
+ ax.text(
+ 0.2,
+ machines_y + y_off,
+ job,
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY1, "edgecolor": LN},
+ )
+ # Dashed flow line
+ ax.annotate(
+ "",
+ xy=(5.5, machines_y + y_off * 0.3),
+ xytext=(0.5, machines_y + y_off),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": "#888888",
+ "lw": 0.8,
+ "linestyle": "dashed",
+ },
+ )
+
+ ax.text(
+ 3,
+ 1.2,
+ "Wszystkie zadania:\nM1 → M2 → M3",
+ ha="center",
+ va="center",
+ fontsize=8,
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ ax.text(
+ 3,
+ 0.4,
+ "Jak taśma montażowa",
+ ha="center",
+ fontsize=7,
+ style="italic",
+ color="#666666",
+ )
+
+ # --- RIGHT: Job Shop ---
+ ax = axes[1]
+ ax.set_xlim(0, 6)
+ ax.set_ylim(0, 6)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title("Job Shop (Jm)", fontsize=10, fontweight="bold", pad=8)
+
+ # Machines scattered
+ m_positions = [(1.5, 4.2), (4.5, 4.2), (3, 2.5)]
+
+ for i, (mx, my) in enumerate(m_positions):
+ circle = plt.Circle((mx, my), mach_r, facecolor=GRAY2, edgecolor=LN, lw=1.5)
+ ax.add_patch(circle)
+ ax.text(
+ mx, my, f"M{i + 1}", ha="center", va="center", fontsize=9, fontweight="bold"
+ )
+
+ # J1: M1 → M2 → M3 (solid)
+ route1 = [(1.5, 4.2), (4.5, 4.2), (3, 2.5)]
+ for i in range(len(route1) - 1):
+ x1, y1 = route1[i]
+ x2, y2 = route1[i + 1]
+ dx = x2 - x1
+ dy = y2 - y1
+ d = (dx**2 + dy**2) ** 0.5
+ draw_arrow(
+ ax,
+ x1 + mach_r * dx / d + 0.05,
+ y1 + mach_r * dy / d,
+ x2 - mach_r * dx / d - 0.05,
+ y2 - mach_r * dy / d,
+ lw=1.5,
+ )
+ ax.text(
+ 0.3,
+ 4.8,
+ "J1: M1→M2→M3",
+ fontsize=7,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.1", "facecolor": GRAY1, "edgecolor": LN},
+ )
+
+ # J2: M2 → M3 → M1 (dashed)
+ route2 = [(4.5, 4.2), (3, 2.5), (1.5, 4.2)]
+ for i in range(len(route2) - 1):
+ x1, y1 = route2[i]
+ x2, y2 = route2[i + 1]
+ dx = x2 - x1
+ dy = y2 - y1
+ d = (dx**2 + dy**2) ** 0.5
+ off = 0.15 # offset to avoid overlap
+ ax.annotate(
+ "",
+ xy=(x2 - mach_r * dx / d - 0.05, y2 - mach_r * dy / d + off),
+ xytext=(x1 + mach_r * dx / d + 0.05, y1 + mach_r * dy / d + off),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": "#555555",
+ "lw": 1.5,
+ "linestyle": "dashed",
+ },
+ )
+ ax.text(
+ 3.8,
+ 5.2,
+ "J2: M2→M3→M1",
+ fontsize=7,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.1", "facecolor": GRAY4, "edgecolor": LN},
+ )
+
+ ax.text(
+ 3,
+ 1.2,
+ "Każde zadanie:\nwłasna trasa!",
+ ha="center",
+ va="center",
+ fontsize=8,
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ ax.text(
+ 3,
+ 0.4,
+ "NP-trudny już dla 3 maszyn",
+ ha="center",
+ fontsize=7,
+ style="italic",
+ color="#666666",
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "scheduling_flow_vs_job.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ scheduling_flow_vs_job.png")
+
+
+# ============================================================
+# 5. SCHEDULING COMPLEXITY LANDSCAPE
+# ============================================================
+def draw_complexity_map() -> None:
+ """Draw complexity map."""
+ _fig, ax = plt.subplots(1, 1, figsize=(8.27, 5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Złożoność problemów szeregowania — od łatwych do NP-trudnych",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Gradient arrow at the top
+ ax.annotate(
+ "",
+ xy=(9.5, 6.2),
+ xytext=(0.5, 6.2),
+ arrowprops={"arrowstyle": "->", "color": LN, "lw": 2},
+ )
+ ax.text(5, 6.5, "Rosnąca złożoność", ha="center", fontsize=9, fontweight="bold")
+
+ # Easy (polynomial) region
+ easy_rect = FancyBboxPatch(
+ (0.3, 2.8),
+ 4.0,
+ 3.0,
+ boxstyle="round,pad=0.15",
+ lw=1.5,
+ edgecolor="#666666",
+ facecolor=GRAY4,
+ linestyle="-",
+ )
+ ax.add_patch(easy_rect)
+ ax.text(
+ 2.3,
+ 5.5,
+ "WIELOMIANOWE O(n log n)",
+ ha="center",
+ fontsize=9,
+ fontweight="bold",
+ color="#444444",
+ )
+
+ easy_problems = [
+ ("1 || ΣCⱼ", "SPT", GRAY1, 4.8),
+ ("1 || Lmax", "EDD", GRAY2, 4.0),
+ ("F2 || Cmax", "Johnson", GRAY1, 3.2),
+ ]
+ for prob, method, fill, y in easy_problems:
+ rect = FancyBboxPatch(
+ (0.6, y),
+ 3.5,
+ 0.6,
+ boxstyle="round,pad=0.05",
+ lw=1,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 1.2,
+ y + 0.3,
+ prob,
+ ha="center",
+ va="center",
+ fontsize=8,
+ fontweight="bold",
+ fontfamily="monospace",
+ )
+ ax.text(3.0, y + 0.3, f"→ {method}", ha="center", va="center", fontsize=8)
+
+ # Hard (NP) region
+ hard_rect = FancyBboxPatch(
+ (5.3, 2.8),
+ 4.3,
+ 3.0,
+ boxstyle="round,pad=0.15",
+ lw=1.5,
+ edgecolor="#444444",
+ facecolor=GRAY3,
+ linestyle="-",
+ )
+ ax.add_patch(hard_rect)
+ ax.text(
+ 7.45,
+ 5.5,
+ "NP-TRUDNE",
+ ha="center",
+ fontsize=9,
+ fontweight="bold",
+ color="#333333",
+ )
+
+ hard_problems = [
+ ("Pm || Cmax\n(m≥2)", "LPT heuryst.", GRAY2, 4.5),
+ ("1 || ΣTⱼ", "branch&bound", GRAY4, 3.7),
+ ("Jm || Cmax\n(m≥3)", "metaheuryst.", GRAY5, 2.9),
+ ]
+ for prob, method, fill, y in hard_problems:
+ rect = FancyBboxPatch(
+ (5.6, y),
+ 3.7,
+ 0.7,
+ boxstyle="round,pad=0.05",
+ lw=1,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ 6.5,
+ y + 0.35,
+ prob,
+ ha="center",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ fontfamily="monospace",
+ )
+ ax.text(8.2, y + 0.35, f"→ {method}", ha="center", va="center", fontsize=7)
+
+ # Arrow connecting
+ draw_arrow(ax, 4.4, 4.0, 5.2, 4.0, lw=2, color="#888888")
+ ax.text(4.8, 4.25, "+1\nmaszyna", ha="center", fontsize=6, color="#888888")
+
+ # Bottom: key insight
+ ax.text(
+ 5.0,
+ 1.8,
+ "„Dodanie jednej maszyny lub jednego ograniczenia\n"
+ 'może zmienić problem z łatwego na NP-trudny!"',
+ ha="center",
+ fontsize=8,
+ fontweight="bold",
+ style="italic",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ "lw": 1,
+ },
+ )
+
+ # Bottom examples
+ ax.text(
+ 5.0,
+ 0.8,
+ "1 maszyna → łatwe (sortuj) | ≥2 maszyny równoległe → NP-trudne\n"
+ "Flow shop 2 maszyny → Johnson O(n log n) | Flow shop 3 maszyny → NP-trudne",
+ ha="center",
+ fontsize=7,
+ color="#555555",
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "scheduling_complexity_map.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ scheduling_complexity_map.png")
+
+
+# ============================================================
+# 6. EDD EXAMPLE (1 || Lmax)
+# ============================================================
+def draw_edd_example() -> None:
+ """Draw edd example."""
+ _fig, ax = plt.subplots(1, 1, figsize=(8.27, 4))
+ ax.set_xlim(-2, 28)
+ ax.set_ylim(-2, 4)
+ ax.axis("off")
+ ax.set_title(
+ "EDD (Earliest Due Date) — 1 || Lmax — Przykład",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=8,
+ )
+
+ # Tasks: name, processing time, due date
+ tasks = [("J1", 4, 10), ("J2", 2, 6), ("J3", 6, 15), ("J4", 3, 8), ("J5", 5, 18)]
+ # EDD: sort by due date
+ edd_order = sorted(tasks, key=lambda x: x[2])
+
+ bar_y = 1.5
+ bar_h = 0.8
+ t = 0
+ fills_edd = [GRAY1, GRAY2, GRAY4, GRAY3, GRAY5]
+
+ lateness_vals = []
+ for i, (name, p, d) in enumerate(edd_order):
+ rect = mpatches.Rectangle(
+ (t, bar_y), p, bar_h, lw=1.2, edgecolor=LN, facecolor=fills_edd[i]
+ )
+ ax.add_patch(rect)
+ ax.text(
+ t + p / 2,
+ bar_y + bar_h / 2,
+ f"{name}\np={p}, d={d}",
+ ha="center",
+ va="center",
+ fontsize=6.5,
+ fontweight="bold",
+ )
+ t += p
+ L = t - d
+ lateness_vals.append(L)
+
+ # Due date marker
+ ax.plot(
+ [d, d], [bar_y - 0.4, bar_y - 0.1], color="#888888", lw=0.8, linestyle="--"
+ )
+ ax.text(
+ d,
+ bar_y - 0.5,
+ f"d={d}",
+ ha="center",
+ va="top",
+ fontsize=5.5,
+ color="#888888",
+ )
+
+ # Completion + lateness
+ ax.plot([t, t], [bar_y + bar_h, bar_y + bar_h + 0.15], color=LN, lw=0.8)
+ ax.text(
+ t,
+ bar_y + bar_h + 0.2,
+ f"C={t}\nL={L}",
+ ha="center",
+ va="bottom",
+ fontsize=5.5,
+ )
+
+ # Time axis
+ ax.plot([0, 22], [bar_y - 0.05, bar_y - 0.05], color=LN, lw=0.5)
+
+ Lmax = max(lateness_vals)
+ ax.text(
+ 22,
+ bar_y + bar_h / 2,
+ f"Lmax = {Lmax}",
+ ha="left",
+ va="center",
+ fontsize=10,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY1, "edgecolor": LN},
+ )
+
+ # Bottom mnemonic
+ ax.text(
+ 10,
+ -1.3,
+ '„Early Due Date Does it first" — najpilniejszy deadline idzie pierwszy',
+ ha="center",
+ fontsize=8,
+ fontweight="bold",
+ style="italic",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": GRAY4,
+ "edgecolor": GRAY3,
+ "lw": 0.8,
+ },
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "scheduling_edd_example.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ scheduling_edd_example.png")
+
+
+# ============================================================
+# MAIN
+# ============================================================
+if __name__ == "__main__":
+ print("Generating scheduling diagrams for PYTANIE 17...")
+ draw_graham_notation()
+ draw_johnson_gantt()
+ draw_spt_comparison()
+ draw_flow_vs_job()
+ draw_complexity_map()
+ draw_edd_example()
+ print("Done! All diagrams saved to:", OUTPUT_DIR)
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_shortest_path_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_shortest_path_diagrams.py
new file mode 100755
index 0000000..2925b5a
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_shortest_path_diagrams.py
@@ -0,0 +1,593 @@
+#!/usr/bin/env python3
+"""Generate diagrams for PYTANIE 2: Shortest path algorithms.
+
+ 1. Graph structure — the shared example graph (A,B,C,D)
+ 2. Dijkstra traversal — step-by-step on that graph
+ 3. Bellman-Ford traversal — step-by-step
+ 4. A* traversal — step-by-step with heuristics.
+
+All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 11
+FS_SMALL = 6.5
+FS_EDGE = 9
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+LIGHT_GREEN = "#D5E8D4"
+LIGHT_RED = "#F8D7DA"
+LIGHT_BLUE = "#D6EAF8"
+LIGHT_YELLOW = "#FFF9C4"
+LIGHT_ORANGE = "#FFE0B2"
+LIGHT_PURPLE = "#E8D5F5"
+
+# --- Shared graph layout ---
+# Graph: A--2--B--3--D, A--4--C--5--D, D--1--C (directed: C->D has weight 5)
+NODE_POS = {"A": (1, 2), "B": (3.5, 3.2), "C": (1, 0), "D": (3.5, 0.8)}
+EDGES = [("A", "B", 2), ("A", "C", 4), ("B", "D", 3), ("C", "D", 5)]
+
+
+def draw_graph_node(
+ ax,
+ name,
+ pos,
+ color="white",
+ current=False,
+ visited=False,
+ dist_label=None,
+ fontsize=12,
+) -> None:
+ """Draw a graph node (circle with label)."""
+ x, y = pos
+ r = 0.35
+ lw = 2.5 if current else 1.5
+ ec = "#D32F2F" if current else LN
+ fc = LIGHT_GREEN if visited else color
+ if current:
+ fc = LIGHT_YELLOW
+
+ circle = plt.Circle(
+ (x, y), r, fill=True, facecolor=fc, edgecolor=ec, linewidth=lw, zorder=5
+ )
+ ax.add_patch(circle)
+ ax.text(
+ x,
+ y,
+ name,
+ ha="center",
+ va="center",
+ fontsize=fontsize,
+ fontweight="bold",
+ zorder=6,
+ )
+
+ if dist_label is not None:
+ ax.text(
+ x,
+ y - 0.55,
+ f"d={dist_label}",
+ ha="center",
+ va="center",
+ fontsize=FS,
+ zorder=6,
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "white",
+ "edgecolor": GRAY3,
+ "alpha": 0.95,
+ },
+ )
+
+
+def draw_graph_edge(ax, pos1, pos2, weight, highlighted=False, relaxed=False) -> None:
+ """Draw an edge between two nodes with weight label."""
+ x1, y1 = pos1
+ x2, y2 = pos2
+
+ # Shorten line to not overlap node circles
+ dx, dy = x2 - x1, y2 - y1
+ length = np.sqrt(dx**2 + dy**2)
+ r = 0.38
+ sx = x1 + r * dx / length
+ sy = y1 + r * dy / length
+ ex = x2 - r * dx / length
+ ey = y2 - r * dy / length
+
+ color = "#D32F2F" if relaxed else ("#1565C0" if highlighted else GRAY3)
+ lw = 2.5 if (highlighted or relaxed) else 1.5
+ ls = "-"
+
+ ax.plot([sx, ex], [sy, ey], color=color, linewidth=lw, linestyle=ls, zorder=2)
+
+ # Weight label
+ mx = (x1 + x2) / 2
+ my = (y1 + y2) / 2
+ # Offset perpendicular to edge
+ perp_x = -dy / length * 0.2
+ perp_y = dx / length * 0.2
+
+ ax.text(
+ mx + perp_x,
+ my + perp_y,
+ str(weight),
+ ha="center",
+ va="center",
+ fontsize=FS_EDGE,
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.15",
+ "facecolor": "white",
+ "edgecolor": GRAY3 if not highlighted else color,
+ "alpha": 0.95,
+ },
+ zorder=4,
+ )
+
+
+def draw_full_graph(
+ ax,
+ title="",
+ dist=None,
+ current=None,
+ visited=None,
+ highlighted_edges=None,
+ relaxed_edges=None,
+) -> None:
+ """Draw the complete graph with optional highlighting."""
+ if visited is None:
+ visited = set()
+ if highlighted_edges is None:
+ highlighted_edges = set()
+ if relaxed_edges is None:
+ relaxed_edges = set()
+ if dist is None:
+ dist = {}
+
+ ax.set_xlim(-0.2, 4.7)
+ ax.set_ylim(-0.8, 4.2)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ if title:
+ ax.set_title(title, fontsize=FS, fontweight="bold", pad=5)
+
+ # Draw edges
+ for u, v, w in EDGES:
+ hl = (u, v) in highlighted_edges or (v, u) in highlighted_edges
+ rl = (u, v) in relaxed_edges or (v, u) in relaxed_edges
+ draw_graph_edge(ax, NODE_POS[u], NODE_POS[v], w, highlighted=hl, relaxed=rl)
+
+ # Draw nodes
+ for name, pos in NODE_POS.items():
+ is_current = name == current
+ is_visited = name in visited
+ d_label = dist.get(name)
+ draw_graph_node(
+ ax, name, pos, current=is_current, visited=is_visited, dist_label=d_label
+ )
+
+
+# ============================================================
+# 1. Graph structure diagram
+# ============================================================
+def draw_graph_structure() -> None:
+ """The shared example graph used across all three algorithms."""
+ _fig, ax = plt.subplots(1, 1, figsize=(5, 4))
+ ax.set_xlim(-0.5, 5.0)
+ ax.set_ylim(-1.2, 4.5)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ "Przykładowy graf — wspólny dla wszystkich algorytmów\n"
+ "Wierzchołki: {A, B, C, D}, Start = A",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Draw edges
+ for u, v, w in EDGES:
+ draw_graph_edge(ax, NODE_POS[u], NODE_POS[v], w)
+
+ # Draw nodes
+ for name, pos in NODE_POS.items():
+ draw_graph_node(ax, name, pos)
+
+ # Start arrow
+ ax.annotate(
+ "START",
+ xy=(NODE_POS["A"][0] - 0.35, NODE_POS["A"][1]),
+ xytext=(NODE_POS["A"][0] - 1.2, NODE_POS["A"][1]),
+ fontsize=FS,
+ fontweight="bold",
+ color="#D32F2F",
+ arrowprops={"arrowstyle": "->", "color": "#D32F2F", "lw": 2},
+ va="center",
+ )
+
+ # Edge list
+ ax.text(
+ 2.3,
+ -0.8,
+ "Krawędzie: A→B(2), A→C(4), B→D(3), C→D(5)\n|V|=4, |E|=4, wagi ≥ 0",
+ ha="center",
+ va="center",
+ fontsize=FS,
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "graph_example_structure.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ graph_example_structure.png")
+
+
+# ============================================================
+# 2. Dijkstra traversal
+# ============================================================
+def draw_dijkstra_traversal() -> None:
+ """Step-by-step Dijkstra on the shared graph."""
+ steps = [
+ {
+ "title": "Krok 0: Inicjalizacja\nd = {A:0, B:∞, C:∞, D:∞}",
+ "dist": {"A": "0", "B": "∞", "C": "∞", "D": "∞"},
+ "current": "A",
+ "visited": set(),
+ "highlighted": set(),
+ "relaxed": set(),
+ },
+ {
+ "title": "Krok 1: Przetwarzam A (d=0)\nRelaksacja: A→B: 0+2=2<∞ ✓ A→C: 0+4=4<∞ ✓",
+ "dist": {"A": "0", "B": "2", "C": "4", "D": "∞"},
+ "current": "A",
+ "visited": {"A"},
+ "highlighted": set(),
+ "relaxed": {("A", "B"), ("A", "C")},
+ },
+ {
+ "title": "Krok 2: Przetwarzam B (d=2) — minimum\nRelaksacja: B→D: 2+3=5<∞ ✓",
+ "dist": {"A": "0", "B": "2", "C": "4", "D": "5"},
+ "current": "B",
+ "visited": {"A", "B"},
+ "highlighted": set(),
+ "relaxed": {("B", "D")},
+ },
+ {
+ "title": "Krok 3: Przetwarzam C (d=4)\nRelaksacja: C→D: 4+5=9 > 5 ✗ (nie poprawia)",
+ "dist": {"A": "0", "B": "2", "C": "4", "D": "5"},
+ "current": "C",
+ "visited": {"A", "B", "C"},
+ "highlighted": {("C", "D")},
+ "relaxed": set(),
+ },
+ {
+ "title": "Krok 4: WYNIK — wszystkie przetworzone\nd = {A:0, B:2, C:4, D:5}",
+ "dist": {"A": "0", "B": "2", "C": "4", "D": "5"},
+ "current": None,
+ "visited": {"A", "B", "C", "D"},
+ "highlighted": {("A", "B"), ("B", "D"), ("A", "C")},
+ "relaxed": set(),
+ },
+ ]
+
+ fig, axes = plt.subplots(1, 5, figsize=(14, 3.5))
+ fig.suptitle(
+ "Dijkstra — przejście grafu krok po kroku (zachłannie: zawsze bierz min d)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=1.02,
+ )
+
+ for _i, (ax, step) in enumerate(zip(axes, steps, strict=False)):
+ draw_full_graph(
+ ax,
+ title=step["title"],
+ dist=step["dist"],
+ current=step["current"],
+ visited=step["visited"],
+ highlighted_edges=step["highlighted"],
+ relaxed_edges=step["relaxed"],
+ )
+
+ # Legend
+ fig.text(
+ 0.5,
+ -0.04,
+ "[zolty] = aktualnie przetwarzany [zielony] = odwiedzony (zamkniety) "
+ "czerwona krawedz = relaksacja OK szara krawedz = nie poprawia",
+ ha="center",
+ fontsize=FS,
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "dijkstra_traversal.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ dijkstra_traversal.png")
+
+
+# ============================================================
+# 3. Bellman-Ford traversal
+# ============================================================
+def draw_bellman_ford_traversal() -> None:
+ """Step-by-step Bellman-Ford on the shared graph."""
+ fig = plt.figure(figsize=(14, 7))
+ fig.suptitle(
+ "Bellman-Ford — przejście grafu krok po kroku\n"
+ "(V-1 = 3 iteracje, w każdej relaksuj WSZYSTKIE krawędzie)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=0.98,
+ )
+
+ # Data for each iteration
+ iterations = [
+ {
+ "title": "Inicjalizacja",
+ "edges_detail": "—",
+ "dist": {"A": "0", "B": "∞", "C": "∞", "D": "∞"},
+ "relaxed": set(),
+ },
+ {
+ "title": "Iteracja 1 (V-1=3)",
+ "edges_detail": (
+ "A→B: 0+2=2<∞ ✓\nA→C: 0+4=4<∞ ✓\nB→D: 2+3=5<∞ ✓\nC→D: 4+5=9>5 ✗"
+ ),
+ "dist": {"A": "0", "B": "2", "C": "4", "D": "5"},
+ "relaxed": {("A", "B"), ("A", "C"), ("B", "D")},
+ },
+ {
+ "title": "Iteracja 2",
+ "edges_detail": (
+ "A→B: 0+2=2=2 ✗\nA→C: 0+4=4=4 ✗\nB→D: 2+3=5=5 ✗\nC→D: 4+5=9>5 ✗"
+ ),
+ "dist": {"A": "0", "B": "2", "C": "4", "D": "5"},
+ "relaxed": set(),
+ },
+ {
+ "title": "Iteracja 3",
+ "edges_detail": (
+ "Brak zmian → stabilne!\n(wczesne zakończenie\n optymalizacja)"
+ ),
+ "dist": {"A": "0", "B": "2", "C": "4", "D": "5"},
+ "relaxed": set(),
+ },
+ ]
+
+ for i, it in enumerate(iterations):
+ # Graph subplot
+ ax_g = fig.add_subplot(2, 4, i + 1)
+ draw_full_graph(
+ ax_g,
+ title=it["title"],
+ dist=it["dist"],
+ current=None,
+ visited=set() if i == 0 else {"A", "B", "C", "D"},
+ relaxed_edges=it["relaxed"],
+ )
+
+ # Detail subplot below
+ ax_d = fig.add_subplot(2, 4, i + 5)
+ ax_d.axis("off")
+ ax_d.text(
+ 0.5,
+ 0.5,
+ it["edges_detail"],
+ ha="center",
+ va="center",
+ fontsize=FS,
+ family="monospace",
+ bbox={"boxstyle": "round,pad=0.4", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # Negative cycle check note
+ fig.text(
+ 0.5,
+ 0.01,
+ "Po 3 iteracjach: sprawdź raz jeszcze — nic się nie zmienia → BRAK cyklu ujemnego → wynik poprawny",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": LIGHT_GREEN, "edgecolor": LN},
+ )
+
+ plt.tight_layout(rect=[0, 0.05, 1, 0.95])
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "bellman_ford_traversal.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ bellman_ford_traversal.png")
+
+
+# ============================================================
+# 4. A* traversal
+# ============================================================
+def draw_astar_traversal() -> None:
+ """Step-by-step A* on the shared graph with heuristics."""
+ # Heuristic values (straight-line distance to D)
+ h_vals = {"A": 4, "B": 2, "C": 3, "D": 0}
+
+ fig = plt.figure(figsize=(14, 7.5))
+ fig.suptitle(
+ "A* — przejście grafu krok po kroku (cel = D)\n"
+ "f(n) = g(n) + h(n), heurystyka h = oszacowana odległość do D",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=0.99,
+ )
+
+ steps = [
+ {
+ "title": "Krok 0: Inicjalizacja\nh(A)=4, h(B)=2, h(C)=3, h(D)=0",
+ "detail": (
+ "g(A)=0, f(A)=0+4=4\npq = [(4, A)]\nh = oszacowanie\n odl. do celu D"
+ ),
+ "dist": {"A": "0"},
+ "f_vals": {"A": "f=4"},
+ "current": "A",
+ "visited": set(),
+ "relaxed": set(),
+ },
+ {
+ "title": "Krok 1: pop A (f=4)\nA→B: g=2, f=2+2=4\nA→C: g=4, f=4+3=7",
+ "detail": (
+ "Relaksacja:\n"
+ " A→B: g=0+2=2\n"
+ " f=2+h(B)=2+2=4\n"
+ " A→C: g=0+4=4\n"
+ " f=4+h(C)=4+3=7\n"
+ "pq = [(4,B), (7,C)]"
+ ),
+ "dist": {"A": "0", "B": "2", "C": "4"},
+ "current": "A",
+ "visited": {"A"},
+ "relaxed": {("A", "B"), ("A", "C")},
+ },
+ {
+ "title": "Krok 2: pop B (f=4) — min!\nB→D: g=5, f=5+0=5",
+ "detail": (
+ "B ma f=4 < C(f=7)\n"
+ "→ A* kieruje się\n"
+ " W STRONĘ celu!\n"
+ "Relaksacja:\n"
+ " B→D: g=2+3=5\n"
+ " f=5+h(D)=5+0=5\n"
+ "pq = [(5,D), (7,C)]"
+ ),
+ "dist": {"A": "0", "B": "2", "C": "4", "D": "5"},
+ "current": "B",
+ "visited": {"A", "B"},
+ "relaxed": {("B", "D")},
+ },
+ {
+ "title": "Krok 3: pop D (f=5)\nu == goal → STOP!",
+ "detail": (
+ "D to CEL → KONIEC!\n"
+ "Nie przetwarzamy C\n"
+ " (f(C)=7 > f(D)=5)\n\n"
+ "Ścieżka: A→B→D\n"
+ "Koszt: 5\n\n"
+ "Dijkstra odwi-\n"
+ "edziłby też C!"
+ ),
+ "dist": {"A": "0", "B": "2", "D": "5"},
+ "current": "D",
+ "visited": {"A", "B", "D"},
+ "relaxed": set(),
+ },
+ ]
+
+ for i, step in enumerate(steps):
+ # Graph
+ ax_g = fig.add_subplot(2, 4, i + 1)
+ draw_full_graph(
+ ax_g,
+ title=step["title"],
+ dist=step["dist"],
+ current=step["current"],
+ visited=step["visited"],
+ relaxed_edges=step["relaxed"],
+ )
+
+ # Add h values as small labels
+ for name, pos in NODE_POS.items():
+ ax_g.text(
+ pos[0] + 0.35,
+ pos[1] + 0.35,
+ f"h={h_vals[name]}",
+ ha="center",
+ va="center",
+ fontsize=5.5,
+ color="#1565C0",
+ fontweight="bold",
+ zorder=7,
+ bbox={
+ "boxstyle": "round,pad=0.1",
+ "facecolor": LIGHT_BLUE,
+ "edgecolor": "#1565C0",
+ "alpha": 0.9,
+ "lw": 0.5,
+ },
+ )
+
+ # Detail
+ ax_d = fig.add_subplot(2, 4, i + 5)
+ ax_d.axis("off")
+ ax_d.text(
+ 0.5,
+ 0.5,
+ step["detail"],
+ ha="center",
+ va="center",
+ fontsize=FS,
+ family="monospace",
+ bbox={"boxstyle": "round,pad=0.4", "facecolor": GRAY4, "edgecolor": GRAY3},
+ )
+
+ # Comparison note
+ fig.text(
+ 0.5,
+ 0.01,
+ "A* odwiedził 3 wierzchołki (A, B, D) — POMINĄŁ C!\n"
+ "Dijkstra odwiedziłby wszystkie 4. Heurystyka h kieruje przeszukiwanie w stronę celu.",
+ ha="center",
+ fontsize=FS,
+ fontweight="bold",
+ bbox={
+ "boxstyle": "round,pad=0.3",
+ "facecolor": LIGHT_BLUE,
+ "edgecolor": "#1565C0",
+ },
+ )
+
+ plt.tight_layout(rect=[0, 0.06, 1, 0.95])
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "astar_traversal.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ astar_traversal.png")
+
+
+# ============================================================
+# Main
+# ============================================================
+if __name__ == "__main__":
+ print("Generating shortest path diagrams for PYTANIE 2...")
+ draw_graph_structure()
+ draw_dijkstra_traversal()
+ draw_bellman_ford_traversal()
+ draw_astar_traversal()
+ print(f"\nAll diagrams saved to {OUTPUT_DIR}/")
diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_study_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_study_diagrams.py
new file mode 100755
index 0000000..a0c1d70
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/generate_study_diagrams.py
@@ -0,0 +1,1064 @@
+#!/usr/bin/env python3
+"""Generate study diagrams for defense preparation.
+
+ 1. PYTANIE 12: Network optimization models (mnemonic overview)
+ 2. PYTANIE 21: Vector clock timeline
+ 3. PYTANIE 22: Linearizability vs Sequential consistency, Paxos flow
+ 4. PYTANIE 23: Segmentation types and over-segmentation
+ 5. PYTANIE 24: HOG pipeline, SVM margin, R-CNN vs YOLO architecture.
+
+All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
+"""
+
+import matplotlib as mpl
+
+mpl.use("Agg")
+from pathlib import Path
+
+import matplotlib.patches as mpatches
+from matplotlib.patches import FancyBboxPatch
+import matplotlib.pyplot as plt
+import numpy as np
+from scipy.stats import norm
+
+DPI = 300
+BG = "white"
+LN = "black"
+FS = 8
+FS_TITLE = 12
+OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
+Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
+
+GRAY1 = "#E8E8E8"
+GRAY2 = "#D0D0D0"
+GRAY3 = "#B8B8B8"
+GRAY4 = "#F5F5F5"
+GRAY5 = "#C0C0C0"
+
+
+def draw_box(
+ ax,
+ x,
+ y,
+ w,
+ h,
+ text,
+ fill="white",
+ lw=1.2,
+ fontsize=FS,
+ fontweight="normal",
+ ha="center",
+ va="center",
+ rounded=True,
+) -> None:
+ """Draw box."""
+ if rounded:
+ rect = FancyBboxPatch(
+ (x, y), w, h, boxstyle="round,pad=0.05", lw=lw, edgecolor=LN, facecolor=fill
+ )
+ else:
+ rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
+ ax.add_patch(rect)
+ ax.text(
+ x + w / 2,
+ y + h / 2,
+ text,
+ ha=ha,
+ va=va,
+ fontsize=fontsize,
+ fontweight=fontweight,
+ wrap=True,
+ )
+
+
+def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None:
+ """Draw arrow."""
+ ax.annotate(
+ "",
+ xy=(x2, y2),
+ xytext=(x1, y1),
+ arrowprops={"arrowstyle": style, "color": color, "lw": lw},
+ )
+
+
+# ============================================================
+# PYTANIE 12: Network Optimization Models (Mnemonic Overview)
+# ============================================================
+def draw_network_models() -> None:
+ """Draw network models."""
+ _fig, ax = plt.subplots(1, 1, figsize=(8.27, 5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 7)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(
+ 'Sieciowe modele optymalizacji — „Nasz Mały Mikołaj Przydzielił Trasy Ciężarówkom Mapując"',
+ fontsize=10,
+ fontweight="bold",
+ pad=10,
+ )
+
+ models = [
+ (
+ 1,
+ "Najkrótsza\nścieżka",
+ "GPS, routing\nDijkstra, A*",
+ "A→B najszybciej?",
+ GRAY1,
+ ),
+ (
+ 2,
+ "Maksymalny\nprzepływ",
+ "Przepustowość\nFord-Fulkerson",
+ "Ile max przesłać?",
+ GRAY4,
+ ),
+ (
+ 3,
+ "Min koszt\nprzepływu",
+ "Najtańszy transport\nSieciowy simpleks",
+ "X jednostek najtaniej?",
+ GRAY4,
+ ),
+ (
+ 4,
+ "Przydział\n(assignment)",
+ "n→n, min koszt\nAlg. Węgierski O(n³)",
+ "Kto robi co?",
+ GRAY2,
+ ),
+ (
+ 5,
+ "TSP\n(komiwojażer)",
+ "Objazd miast\nNP-trudny, heurystyki",
+ "Objazd wszystkiego?",
+ GRAY3,
+ ),
+ (6, "CPM/PERT", "Harmonogram\nŚcieżka krytyczna", "Ile trwa projekt?", GRAY2),
+ (
+ 7,
+ "MST\n(drzewo rozp.)",
+ "Min połączenie\nKruskal, Prim",
+ "Połącz najtaniej?",
+ GRAY1,
+ ),
+ ]
+
+ # Layout: 3 pairs + 1, arranged in labeled groups
+ group_positions = [
+ # (group_label, [(model_idx, x, y), ...])
+ ("DROGI", [(0, 0.3, 4.0), (6, 0.3, 1.5)]),
+ ("PRZEPŁYW", [(1, 3.3, 4.0), (2, 3.3, 1.5)]),
+ ("ZARZĄDZANIE", [(3, 6.3, 4.0), (5, 6.3, 1.5)]),
+ ]
+
+ box_w = 2.6
+ box_h = 1.8
+
+ for group_label, items in group_positions:
+ xs = [x for _, x, y in items]
+ ys = [y for _, x, y in items]
+ gx = min(xs) - 0.15
+ gy = min(ys) - 0.3
+ gw = box_w + 0.3
+ gh = max(ys) - min(ys) + box_h + 0.6
+ rect = mpatches.FancyBboxPatch(
+ (gx, gy),
+ gw,
+ gh,
+ boxstyle="round,pad=0.1",
+ lw=0.8,
+ edgecolor=GRAY3,
+ facecolor="white",
+ linestyle="--",
+ )
+ ax.add_patch(rect)
+ ax.text(
+ gx + gw / 2,
+ gy + gh + 0.12,
+ group_label,
+ ha="center",
+ fontsize=8,
+ fontweight="bold",
+ color="#555555",
+ )
+
+ for idx, x, y in items:
+ num, name, detail, question, fill = models[idx]
+ draw_box(ax, x, y, box_w, box_h, "", fill=fill, fontsize=FS)
+ ax.text(
+ x + box_w / 2,
+ y + box_h - 0.25,
+ f"{num}. {name}",
+ ha="center",
+ va="top",
+ fontsize=8,
+ fontweight="bold",
+ )
+ ax.text(
+ x + box_w / 2,
+ y + box_h / 2 - 0.1,
+ detail,
+ ha="center",
+ va="center",
+ fontsize=7,
+ )
+ ax.text(
+ x + box_w / 2,
+ y + 0.2,
+ f'→ „{question}"',
+ ha="center",
+ va="bottom",
+ fontsize=6.5,
+ style="italic",
+ )
+
+ # TSP alone at bottom center
+ idx = 4
+ x, y = 4.5, -0.1
+ num, name, detail, question, fill = models[idx]
+ rect = mpatches.FancyBboxPatch(
+ (x - 0.15, y - 0.15),
+ box_w + 0.3,
+ box_h + 0.3,
+ boxstyle="round,pad=0.1",
+ lw=0.8,
+ edgecolor=GRAY3,
+ facecolor="white",
+ linestyle="--",
+ )
+ ax.add_patch(rect)
+ ax.text(
+ x + box_w / 2,
+ y + box_h + 0.3,
+ "SAM (NP-trudny)",
+ ha="center",
+ fontsize=8,
+ fontweight="bold",
+ color="#555555",
+ )
+ draw_box(ax, x, y, box_w, box_h, "", fill=fill, fontsize=FS)
+ ax.text(
+ x + box_w / 2,
+ y + box_h - 0.25,
+ f"{num}. {name}",
+ ha="center",
+ va="top",
+ fontsize=8,
+ fontweight="bold",
+ )
+ ax.text(
+ x + box_w / 2, y + box_h / 2 - 0.1, detail, ha="center", va="center", fontsize=7
+ )
+ ax.text(
+ x + box_w / 2,
+ y + 0.2,
+ f'→ „{question}"',
+ ha="center",
+ va="bottom",
+ fontsize=6.5,
+ style="italic",
+ )
+
+ ax.set_ylim(-0.5, 7.2)
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "network_models_mnemonic.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ network_models_mnemonic.png")
+
+
+# ============================================================
+# PYTANIE 21: Vector Clock Timeline
+# ============================================================
+def draw_vector_clock_timeline() -> None:
+ """Draw vector clock timeline."""
+ _fig, ax = plt.subplots(1, 1, figsize=(8.27, 4.5))
+ ax.set_xlim(-0.5, 11)
+ ax.set_ylim(-0.5, 4.5)
+ ax.axis("off")
+ ax.set_title(
+ "Zegary wektorowe — przykład z 3 procesami",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Process lines
+ procs = [("P₁", 3.5), ("P₂", 2.0), ("P₃", 0.5)]
+ for name, y in procs:
+ ax.plot([0.5, 10.5], [y, y], color=LN, lw=1.5)
+ ax.text(0.1, y, name, ha="right", va="center", fontsize=10, fontweight="bold")
+
+ # Events
+ events = [
+ # (name, process_y, x, vector, fill)
+ ("A", 3.5, 1.5, "[1,0,0]", GRAY1),
+ ("B", 2.0, 2.5, "[0,1,0]", GRAY2),
+ ("C", 2.0, 5.0, "[1,2,0]", GRAY2),
+ ("D", 0.5, 4.0, "[0,0,1]", GRAY3),
+ ("E", 3.5, 6.5, "[2,0,0]", GRAY1),
+ ("F", 2.0, 8.0, "[2,3,0]", GRAY2),
+ ]
+
+ for name, y, x, vec, fill in events:
+ circle = plt.Circle((x, y), 0.25, facecolor=fill, edgecolor=LN, lw=1.5)
+ ax.add_patch(circle)
+ ax.text(x, y, name, ha="center", va="center", fontsize=9, fontweight="bold")
+ ax.text(
+ x,
+ y + 0.45,
+ vec,
+ ha="center",
+ va="bottom",
+ fontsize=7,
+ fontfamily="monospace",
+ color="#333333",
+ )
+
+ # Messages (arrows between processes)
+ # P1:A → P2:C (msg sent after A, received at C)
+ ax.annotate(
+ "",
+ xy=(4.75, 2.0),
+ xytext=(1.75, 3.5),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": "#444444",
+ "lw": 1.5,
+ "connectionstyle": "arc3,rad=0.05",
+ },
+ )
+ ax.text(3.0, 3.0, "msg₁", ha="center", fontsize=7, color="#444444", style="italic")
+
+ # P1:E → P2:F
+ ax.annotate(
+ "",
+ xy=(7.75, 2.0),
+ xytext=(6.75, 3.5),
+ arrowprops={
+ "arrowstyle": "->",
+ "color": "#444444",
+ "lw": 1.5,
+ "connectionstyle": "arc3,rad=0.05",
+ },
+ )
+ ax.text(7.0, 3.0, "msg₂", ha="center", fontsize=7, color="#444444", style="italic")
+
+ # Concurrency annotations
+ ax.annotate(
+ "A ∥ B\n(współbieżne)",
+ xy=(2.0, 1.2),
+ fontsize=7,
+ ha="center",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+ ax.annotate(
+ "C ∥ D\n(współbieżne)",
+ xy=(4.5, 0.9),
+ fontsize=7,
+ ha="center",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+ ax.annotate(
+ "A → C\n(przyczynowe)",
+ xy=(3.3, 4.2),
+ fontsize=7,
+ ha="center",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY1, "edgecolor": GRAY3},
+ )
+
+ # Time arrow
+ ax.annotate(
+ "",
+ xy=(10.5, -0.3),
+ xytext=(0.5, -0.3),
+ arrowprops={"arrowstyle": "->", "color": GRAY3, "lw": 1.0},
+ )
+ ax.text(5.5, -0.45, "czas →", ha="center", fontsize=8, color="#777777")
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "vector_clock_timeline.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ vector_clock_timeline.png")
+
+
+# ============================================================
+# PYTANIE 22: Linearizability vs Sequential Consistency
+# ============================================================
+def draw_linearizability_vs_sequential() -> None:
+ """Draw linearizability vs sequential."""
+ _fig, axes = plt.subplots(2, 1, figsize=(8.27, 5.5))
+
+ for _i, (ax, title, subtitle, operations, result_text) in enumerate(
+ zip(
+ axes,
+ ["Linearizability", "Sequential Consistency"],
+ [
+ 'Operacja „wygląda" atomowo w czasie rzeczywistym',
+ "Globalny porządek zgodny z programem, ale NIE z czasem rzeczywistym",
+ ],
+ [
+ # Linearizability
+ [
+ ("Klient A", 1, 3, "write(x,1)", GRAY1),
+ ("Klient B", 2, 4, "read(x)→1 ✓", GRAY2),
+ ("Klient A", 5, 7, "write(x,2)", GRAY1),
+ ],
+ # Sequential consistency
+ [
+ ("Klient A", 1, 3, "write(x,1)", GRAY1),
+ ("Klient B", 2, 4, "read(x)→0 ✓", GRAY2),
+ ("Klient A", 5, 7, "write(x,2)", GRAY1),
+ ],
+ ],
+ [
+ "read MUSI zwrócić 1 (write zakończony w czasie rzeczywistym)",
+ "read MOŻE zwrócić 0 (globalny porządek: read, write(1), write(2))",
+ ],
+ strict=False,
+ )
+ ):
+ ax.set_xlim(0, 9)
+ ax.set_ylim(-0.5, 3.5)
+ ax.axis("off")
+ ax.set_title(f"{title}", fontsize=10, fontweight="bold")
+ ax.text(
+ 4.5, 3.2, subtitle, ha="center", fontsize=7, style="italic", color="#555555"
+ )
+
+ # Time axis
+ ax.plot([0.5, 8.5], [0, 0], color=GRAY3, lw=0.8)
+ for t in range(1, 9):
+ ax.plot([t, t], [-0.05, 0.05], color=GRAY3, lw=0.8)
+ ax.text(t, -0.2, f"t{t}", ha="center", fontsize=6, color="#999999")
+
+ # Client labels
+ clients = list(dict.fromkeys([op[0] for op in operations]))
+ client_y = {c: 1.0 + idx * 1.2 for idx, c in enumerate(clients)}
+
+ for client_name, y_pos in client_y.items():
+ ax.text(
+ 0.3,
+ y_pos,
+ client_name,
+ ha="right",
+ va="center",
+ fontsize=7,
+ fontweight="bold",
+ )
+ ax.plot([0.5, 8.5], [y_pos, y_pos], color=GRAY5, lw=0.5, linestyle=":")
+
+ for client, t_start, t_end, label, fill in operations:
+ y = client_y[client]
+ rect = FancyBboxPatch(
+ (t_start, y - 0.2),
+ t_end - t_start,
+ 0.4,
+ boxstyle="round,pad=0.05",
+ lw=1.2,
+ edgecolor=LN,
+ facecolor=fill,
+ )
+ ax.add_patch(rect)
+ ax.text(
+ (t_start + t_end) / 2, y, label, ha="center", va="center", fontsize=7
+ )
+
+ # Result annotation
+ ax.text(
+ 4.5,
+ -0.45,
+ result_text,
+ ha="center",
+ fontsize=7,
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "linearizability_vs_sequential.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ linearizability_vs_sequential.png")
+
+
+# ============================================================
+# PYTANIE 22: Paxos Protocol Flow
+# ============================================================
+def draw_paxos_flow() -> None:
+ """Draw paxos flow."""
+ _fig, ax = plt.subplots(1, 1, figsize=(8.27, 4))
+ ax.set_xlim(-0.5, 10.5)
+ ax.set_ylim(-0.5, 5)
+ ax.axis("off")
+ ax.set_title(
+ "Paxos — uproszczony przebieg (zapis x=5)",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ # Actors
+ actors = [
+ ("Proposer", 1.5, 4.0, GRAY1),
+ ("A₁", 4.5, 4.0, GRAY2),
+ ("A₂", 6.5, 4.0, GRAY2),
+ ("A₃", 8.5, 4.0, GRAY2),
+ ]
+ for name, x, y, fill in actors:
+ draw_box(
+ ax, x - 0.6, y, 1.2, 0.6, name, fill=fill, fontsize=8, fontweight="bold"
+ )
+
+ # Phase 1: Prepare
+ ax.text(
+ -0.3,
+ 3.5,
+ "FAZA 1\nPrepare",
+ ha="center",
+ fontsize=7,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ y_prep = 3.3
+ for target_x in [4.5, 6.5, 8.5]:
+ draw_arrow(ax, 2.1, y_prep + 0.15, target_x - 0.6, y_prep + 0.15, lw=1.0)
+ ax.text(3.3, y_prep + 0.35, "Prepare(n=1)", fontsize=6, ha="center")
+
+ # Promises back
+ y_prom = 2.7
+ for target_x in [4.5, 6.5]:
+ draw_arrow(
+ ax,
+ target_x - 0.6,
+ y_prom + 0.15,
+ 2.1,
+ y_prom + 0.15,
+ lw=1.0,
+ color="#555555",
+ )
+ ax.text(
+ 3.3, y_prom + 0.35, "Promise(n=1) ✓", fontsize=6, ha="center", color="#555555"
+ )
+ ax.text(8.5, y_prom + 0.15, "(slow)", fontsize=6, ha="center", color="#999999")
+
+ ax.text(
+ 1.5,
+ y_prom - 0.15,
+ "majority\n(2/3) ✓",
+ fontsize=6,
+ ha="center",
+ bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY1, "edgecolor": GRAY3},
+ )
+
+ # Phase 2: Accept
+ ax.text(
+ -0.3,
+ 1.8,
+ "FAZA 2\nAccept",
+ ha="center",
+ fontsize=7,
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
+ )
+
+ y_acc = 1.6
+ for target_x in [4.5, 6.5, 8.5]:
+ draw_arrow(ax, 2.1, y_acc + 0.15, target_x - 0.6, y_acc + 0.15, lw=1.0)
+ ax.text(3.3, y_acc + 0.35, "Accept(n=1, x=5)", fontsize=6, ha="center")
+
+ # Accepted back
+ y_accd = 1.0
+ for target_x in [4.5, 6.5]:
+ draw_arrow(
+ ax,
+ target_x - 0.6,
+ y_accd + 0.15,
+ 2.1,
+ y_accd + 0.15,
+ lw=1.0,
+ color="#555555",
+ )
+ ax.text(3.3, y_accd + 0.35, "Accepted ✓", fontsize=6, ha="center", color="#555555")
+
+ # Result
+ ax.text(
+ 5.0,
+ 0.1,
+ "x=5 UZGODNIONE (majority zaakceptowała) → Linearizable!",
+ fontsize=8,
+ ha="center",
+ fontweight="bold",
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY1, "edgecolor": LN},
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "paxos_flow.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ paxos_flow.png")
+
+
+# ============================================================
+# PYTANIE 24: HOG Pipeline Overview
+# ============================================================
+def draw_hog_pipeline() -> None:
+ """Draw hog pipeline."""
+ _fig, ax = plt.subplots(1, 1, figsize=(8.27, 3.5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 4)
+ ax.axis("off")
+ ax.set_title(
+ "HOG + SVM — pipeline detekcji pieszych",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ pad=10,
+ )
+
+ steps = [
+ (0.3, "Obraz\nwejściowy", GRAY4),
+ (2.1, "Oblicz\ngradienty\n(Gx, Gy)", GRAY1),
+ (3.9, "Podziel na\nkomórki 8x8\nhistogramy", GRAY2),
+ (5.7, "Normalizuj\nw blokach\n2x2", GRAY2),
+ (7.5, "Wektor\ncech\n(3780-dim)", GRAY3),
+ (9.0, "SVM\n→ pieszy\n/ nie", GRAY1),
+ ]
+
+ box_w = 1.5
+ box_h = 1.8
+ y = 1.2
+ for i, (x, text, fill) in enumerate(steps):
+ draw_box(ax, x, y, box_w, box_h, "", fill=fill)
+ ax.text(
+ x + box_w / 2, y + box_h / 2, text, ha="center", va="center", fontsize=7
+ )
+ if i < len(steps) - 1:
+ next_x = steps[i + 1][0]
+ draw_arrow(
+ ax, x + box_w + 0.02, y + box_h / 2, next_x - 0.02, y + box_h / 2
+ )
+
+ # Annotations below
+ annotations = [
+ (0.3 + box_w / 2, "pixel[x+1]-pixel[x-1]"),
+ (2.1 + box_w / 2, "magnitude + direction"),
+ (3.9 + box_w / 2, "9 binów (0°-180°)"),
+ (5.7 + box_w / 2, "L2-normalizacja"),
+ (7.5 + box_w / 2, "wejście do SVM"),
+ (9.0 + box_w / 2, "hiperpłaszczyzna"),
+ ]
+ for x, text in annotations:
+ ax.text(
+ x,
+ y - 0.15,
+ text,
+ ha="center",
+ fontsize=5.5,
+ color="#666666",
+ style="italic",
+ )
+
+ # Title annotations
+ ax.text(
+ 1.05, y + box_h + 0.15, "① Gradient", ha="center", fontsize=7, fontweight="bold"
+ )
+ ax.text(
+ 2.85,
+ y + box_h + 0.15,
+ "② Histogram",
+ ha="center",
+ fontsize=7,
+ fontweight="bold",
+ )
+ ax.text(
+ 4.65,
+ y + box_h + 0.15,
+ "③ Normalize",
+ ha="center",
+ fontsize=7,
+ fontweight="bold",
+ )
+ ax.text(
+ 6.45,
+ y + box_h + 0.15,
+ "④ Feature vec",
+ ha="center",
+ fontsize=7,
+ fontweight="bold",
+ )
+ ax.text(
+ 8.1, y + box_h + 0.15, "⑤ Classify", ha="center", fontsize=7, fontweight="bold"
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "hog_svm_pipeline.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ hog_svm_pipeline.png")
+
+
+# ============================================================
+# PYTANIE 24: R-CNN Evolution
+# ============================================================
+def draw_rcnn_evolution() -> None:
+ """Draw rcnn evolution."""
+ _fig, ax = plt.subplots(1, 1, figsize=(8.27, 5))
+ ax.set_xlim(0, 10)
+ ax.set_ylim(0, 7)
+ ax.axis("off")
+ ax.set_title(
+ "Ewolucja detektorów: R-CNN → Fast R-CNN → Faster R-CNN → YOLO",
+ fontsize=10,
+ fontweight="bold",
+ pad=10,
+ )
+
+ models = [
+ {
+ "name": "R-CNN (2014)",
+ "y": 5.3,
+ "steps": ["Selective\nSearch", "2000x\nCNN", "2000x\nSVM", "NMS"],
+ "speed": "~50 sec/img",
+ "fill": GRAY4,
+ },
+ {
+ "name": "Fast R-CNN (2015)",
+ "y": 3.7,
+ "steps": [
+ "Selective\nSearch",
+ "CNN\n(1x cały)",
+ "ROI\nPooling",
+ "FC + NMS",
+ ],
+ "speed": "~2 sec/img",
+ "fill": GRAY2,
+ },
+ {
+ "name": "Faster R-CNN (2015)",
+ "y": 2.1,
+ "steps": ["CNN\nbackbone", "RPN\n(proposals)", "ROI\nPooling", "FC + NMS"],
+ "speed": "~0.2 sec (5 fps)",
+ "fill": GRAY1,
+ },
+ {
+ "name": "YOLO (2016)",
+ "y": 0.5,
+ "steps": ["CNN\nbackbone", "Siatka\nSxS", "bbox+klasa\nper komórka", "NMS"],
+ "speed": "~7-22 ms (45-155 fps)",
+ "fill": GRAY3,
+ },
+ ]
+
+ for model in models:
+ y = model["y"]
+ ax.text(0.2, y + 0.4, model["name"], fontsize=8, fontweight="bold", va="center")
+ ax.text(0.2, y + 0.05, model["speed"], fontsize=6, va="center", color="#666666")
+
+ bw = 1.5
+ bh = 0.8
+ for i, step in enumerate(model["steps"]):
+ x = 2.5 + i * 1.9
+ draw_box(ax, x, y, bw, bh, step, fill=model["fill"], fontsize=6.5)
+ if i < len(model["steps"]) - 1:
+ draw_arrow(
+ ax, x + bw + 0.02, y + bh / 2, x + 1.9 - 0.02, y + bh / 2, lw=0.8
+ )
+
+ # Speed improvement arrow on right
+ ax.annotate(
+ "",
+ xy=(9.5, 5.7),
+ xytext=(9.5, 0.9),
+ arrowprops={"arrowstyle": "<->", "color": "#555555", "lw": 1.5},
+ )
+ ax.text(
+ 9.7,
+ 3.3,
+ "250x\nszybciej!",
+ fontsize=8,
+ fontweight="bold",
+ ha="center",
+ va="center",
+ rotation=90,
+ color="#555555",
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "rcnn_evolution.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ rcnn_evolution.png")
+
+
+# ============================================================
+# PYTANIE 23: Segmentation types comparison
+# ============================================================
+def draw_segmentation_types() -> None:
+ """Draw segmentation types."""
+ fig, axes = plt.subplots(1, 4, figsize=(8.27, 2.5))
+ fig.suptitle(
+ "Typy segmentacji obrazu", fontsize=FS_TITLE, fontweight="bold", y=1.02
+ )
+
+ titles = [
+ "Obraz wejściowy",
+ "Semantic\nSegmentation",
+ "Instance\nSegmentation",
+ "Panoptic\nSegmentation",
+ ]
+ for ax, title in zip(axes, titles, strict=False):
+ ax.set_xlim(0, 6)
+ ax.set_ylim(0, 6)
+ ax.set_aspect("equal")
+ ax.axis("off")
+ ax.set_title(title, fontsize=8, fontweight="bold", pad=5)
+
+ # Image: sky (top), two cars (bottom), road
+ # Semantic: all sky=one color, all cars=one color, road=one color
+ # Instance: sky=one, car1=distinct, car2=distinct, road=one
+ # Panoptic: both
+
+ # Original image (stylized)
+ ax = axes[0]
+ ax.add_patch(
+ mpatches.Rectangle((0, 4), 6, 2, facecolor="#DDDDDD", edgecolor=LN, lw=0.5)
+ ) # sky
+ ax.text(3, 5, "niebo", ha="center", va="center", fontsize=7)
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 6, 2.5, facecolor="#AAAAAA", edgecolor=LN, lw=0.5)
+ ) # road
+ ax.text(3, 1, "droga", ha="center", va="center", fontsize=7)
+ ax.add_patch(
+ mpatches.Rectangle((0.5, 2), 2, 1.5, facecolor="#888888", edgecolor=LN, lw=0.8)
+ ) # car1
+ ax.text(1.5, 2.75, "auto", ha="center", va="center", fontsize=7, color="white")
+ ax.add_patch(
+ mpatches.Rectangle((3.5, 2), 2, 1.5, facecolor="#666666", edgecolor=LN, lw=0.8)
+ ) # car2
+ ax.text(4.5, 2.75, "auto", ha="center", va="center", fontsize=7, color="white")
+
+ # Semantic: same label for both cars
+ ax = axes[1]
+ ax.add_patch(
+ mpatches.Rectangle((0, 4), 6, 2, facecolor="#E8E8E8", edgecolor=LN, lw=0.5)
+ )
+ ax.text(3, 5, "niebo", ha="center", va="center", fontsize=7)
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 6, 2.5, facecolor="#C8C8C8", edgecolor=LN, lw=0.5)
+ )
+ ax.text(3, 1, "droga", ha="center", va="center", fontsize=7)
+ ax.add_patch(
+ mpatches.Rectangle((0.5, 2), 2, 1.5, facecolor="#888888", edgecolor=LN, lw=0.8)
+ )
+ ax.text(1.5, 2.75, "auto", ha="center", va="center", fontsize=6, color="white")
+ ax.add_patch(
+ mpatches.Rectangle((3.5, 2), 2, 1.5, facecolor="#888888", edgecolor=LN, lw=0.8)
+ )
+ ax.text(4.5, 2.75, "auto", ha="center", va="center", fontsize=6, color="white")
+ ax.text(
+ 3,
+ -0.3,
+ "te same etykiety!",
+ ha="center",
+ fontsize=6,
+ color="#555555",
+ style="italic",
+ )
+
+ # Instance: different labels for cars
+ ax = axes[2]
+ ax.add_patch(
+ mpatches.Rectangle((0, 4), 6, 2, facecolor="#E8E8E8", edgecolor=LN, lw=0.5)
+ )
+ ax.text(3, 5, "—", ha="center", va="center", fontsize=7, color="#999999")
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 6, 2.5, facecolor="#E8E8E8", edgecolor=LN, lw=0.5)
+ )
+ ax.text(3, 1, "—", ha="center", va="center", fontsize=7, color="#999999")
+ ax.add_patch(
+ mpatches.Rectangle((0.5, 2), 2, 1.5, facecolor="#888888", edgecolor=LN, lw=0.8)
+ )
+ ax.text(1.5, 2.75, "auto#1", ha="center", va="center", fontsize=6, color="white")
+ ax.add_patch(
+ mpatches.Rectangle((3.5, 2), 2, 1.5, facecolor="#555555", edgecolor=LN, lw=0.8)
+ )
+ ax.text(4.5, 2.75, "auto#2", ha="center", va="center", fontsize=6, color="white")
+ ax.text(
+ 3,
+ -0.3,
+ "RÓŻNE instancje!",
+ ha="center",
+ fontsize=6,
+ color="#555555",
+ style="italic",
+ )
+
+ # Panoptic: both semantic labels AND instance IDs
+ ax = axes[3]
+ ax.add_patch(
+ mpatches.Rectangle((0, 4), 6, 2, facecolor="#E8E8E8", edgecolor=LN, lw=0.5)
+ )
+ ax.text(3, 5, "niebo (stuff)", ha="center", va="center", fontsize=6)
+ ax.add_patch(
+ mpatches.Rectangle((0, 0), 6, 2.5, facecolor="#C8C8C8", edgecolor=LN, lw=0.5)
+ )
+ ax.text(3, 1, "droga (stuff)", ha="center", va="center", fontsize=6)
+ ax.add_patch(
+ mpatches.Rectangle((0.5, 2), 2, 1.5, facecolor="#888888", edgecolor=LN, lw=0.8)
+ )
+ ax.text(
+ 1.5,
+ 2.75,
+ "auto#1\n(thing)",
+ ha="center",
+ va="center",
+ fontsize=5.5,
+ color="white",
+ )
+ ax.add_patch(
+ mpatches.Rectangle((3.5, 2), 2, 1.5, facecolor="#555555", edgecolor=LN, lw=0.8)
+ )
+ ax.text(
+ 4.5,
+ 2.75,
+ "auto#2\n(thing)",
+ ha="center",
+ va="center",
+ fontsize=5.5,
+ color="white",
+ )
+ ax.text(
+ 3,
+ -0.3,
+ "klasy + instancje!",
+ ha="center",
+ fontsize=6,
+ color="#555555",
+ style="italic",
+ )
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "segmentation_types.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ segmentation_types.png")
+
+
+# ============================================================
+# PYTANIE 32: FSD and SSD visualization
+# ============================================================
+def draw_fsd_ssd() -> None:
+ """Draw fsd ssd."""
+ fig, axes = plt.subplots(1, 2, figsize=(8.27, 3.5))
+ fig.suptitle(
+ "Dominacja stochastyczna — FSD i SSD",
+ fontsize=FS_TITLE,
+ fontweight="bold",
+ y=1.02,
+ )
+
+ # FSD: CDF comparison
+ ax = axes[0]
+ ax.set_title("FSD: F_A(x) ≤ F_B(x) ∀x", fontsize=9, fontweight="bold")
+ x = np.linspace(-2, 6, 200)
+ # A ~ shifted right (better), B ~ shifted left
+ F_A = norm.cdf(x, loc=2.5, scale=1.0)
+ F_B = norm.cdf(x, loc=1.5, scale=1.0)
+ ax.plot(x, F_A, "k-", lw=2, label="F_A (lepsza — niżej)")
+ ax.plot(x, F_B, "k--", lw=2, label="F_B (gorsza — wyżej)")
+ ax.fill_between(x, F_A, F_B, alpha=0.15, color="gray")
+ ax.set_xlabel("x (wynik)", fontsize=8)
+ ax.set_ylabel("F(x) = P(X ≤ x)", fontsize=8)
+ ax.legend(fontsize=7, loc="lower right")
+ ax.text(
+ 0,
+ 0.8,
+ "A ≥_FSD B\nF_A zawsze pod F_B\n→ KAŻDY racjonalny\n wybierze A",
+ fontsize=7,
+ bbox={"boxstyle": "round", "facecolor": GRAY4},
+ )
+ ax.grid(True, alpha=0.3)
+ ax.tick_params(labelsize=7)
+
+ # SSD: CDFs can cross, but integral is less
+ ax = axes[1]
+ ax.set_title(
+ "SSD: ∫F_A ≤ ∫F_B ∀x (CDFs mogą się krzyżować)", fontsize=9, fontweight="bold"
+ )
+ F_A2 = norm.cdf(x, loc=2.0, scale=0.8)
+ F_B2 = norm.cdf(x, loc=2.0, scale=1.5) # same mean, more spread
+ ax.plot(x, F_A2, "k-", lw=2, label="F_A (mniej ryzyka)")
+ ax.plot(x, F_B2, "k--", lw=2, label="F_B (więcej ryzyka)")
+ ax.fill_between(x, F_A2, F_B2, where=F_A2 < F_B2, alpha=0.15, color="gray")
+ ax.fill_between(
+ x, F_A2, F_B2, where=F_A2 >= F_B2, alpha=0.08, color="gray", hatch="///"
+ )
+ ax.set_xlabel("x (wynik)", fontsize=8)
+ ax.set_ylabel("F(x)", fontsize=8)
+ ax.legend(fontsize=7, loc="lower right")
+ ax.text(
+ -1.5,
+ 0.75,
+ "A ≥_SSD B\nCDFs się krzyżują,\nale ∫F_A ≤ ∫F_B\n→ risk-averse\n wybierze A",
+ fontsize=7,
+ bbox={"boxstyle": "round", "facecolor": GRAY4},
+ )
+ ax.grid(True, alpha=0.3)
+ ax.tick_params(labelsize=7)
+
+ plt.tight_layout()
+ plt.savefig(
+ str(Path(OUTPUT_DIR) / "fsd_ssd_comparison.png"),
+ dpi=DPI,
+ bbox_inches="tight",
+ facecolor=BG,
+ )
+ plt.close()
+ print(" ✓ fsd_ssd_comparison.png")
+
+
+# ============================================================
+# Main
+# ============================================================
+if __name__ == "__main__":
+ print("Generating study diagrams...")
+ draw_network_models()
+ draw_vector_clock_timeline()
+ draw_linearizability_vs_sequential()
+ draw_paxos_flow()
+ draw_hog_pipeline()
+ draw_rcnn_evolution()
+ draw_segmentation_types()
+ draw_fsd_ssd()
+ print(f"\nAll diagrams saved to {OUTPUT_DIR}/")
diff --git a/python_pkg/praca_magisterska_video/generate_images/print_questions.sh b/python_pkg/praca_magisterska_video/generate_images/print_questions.sh
new file mode 100755
index 0000000..e8170c8
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/print_questions.sh
@@ -0,0 +1,276 @@
+#!/usr/bin/env bash
+#
+# print_questions.sh — Convert and print exam questions from questions/ folder
+#
+# Usage:
+# ./print_questions.sh [OPTIONS] [QUESTION_NUMBERS...]
+#
+# Examples:
+# ./print_questions.sh # Generate PDF of ALL questions
+# ./print_questions.sh 1 # Generate PDF of question 1
+# ./print_questions.sh 1 3 7 # Generate PDF of questions 1, 3, 7
+# ./print_questions.sh 1-5 # Generate PDF of questions 1 through 5
+# ./print_questions.sh 1 3-5 8 # Mix of individual and ranges
+# ./print_questions.sh --print 1 3 # Generate PDF AND print questions 1, 3
+# ./print_questions.sh --print # Generate PDF AND print ALL questions
+# ./print_questions.sh --list # List available questions
+#
+# Options:
+# --print, -p Send to printer after generating PDF
+# --printer NAME Printer name (default: Brother_HL-1110_series)
+# --list, -l List available questions and exit
+# --output, -o FILE Output PDF path (default: auto-generated in /tmp)
+# --keep, -k Keep intermediate files (for debugging)
+# --help, -h Show this help
+#
+
+set -euo pipefail
+
+# ── Configuration ──────────────────────────────────────────────────────────────
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+QUESTIONS_DIR="${SCRIPT_DIR}/questions"
+PRINTER="Brother_HL-1110_series"
+DO_PRINT=false
+LIST_ONLY=false
+KEEP_TMP=false
+OUTPUT_PDF=""
+QUESTIONS=()
+
+# ── Parse arguments ───────────────────────────────────────────────────────────
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --print|-p)
+ DO_PRINT=true
+ shift
+ ;;
+ --printer)
+ PRINTER="$2"
+ shift 2
+ ;;
+ --list|-l)
+ LIST_ONLY=true
+ shift
+ ;;
+ --output|-o)
+ OUTPUT_PDF="$2"
+ shift 2
+ ;;
+ --keep|-k)
+ KEEP_TMP=true
+ shift
+ ;;
+ --help|-h)
+ head -28 "$0" | tail -26
+ exit 0
+ ;;
+ *)
+ # Parse question numbers and ranges (e.g., "1", "3-5", "13/27")
+ if [[ "$1" =~ ^[0-9]+(\/[0-9]+)?(-[0-9]+(\/[0-9]+)?)?$ ]]; then
+ if [[ "$1" == *-* ]]; then
+ # Range: extract start-end
+ range_start="${1%%-*}"
+ range_end="${1##*-}"
+ for ((i=range_start; i<=range_end; i++)); do
+ QUESTIONS+=("$i")
+ done
+ else
+ QUESTIONS+=("$1")
+ fi
+ else
+ echo "Error: Unknown argument '$1'. Use --help for usage." >&2
+ exit 1
+ fi
+ shift
+ ;;
+ esac
+done
+
+# ── Verify questions directory ────────────────────────────────────────────────
+if [[ ! -d "$QUESTIONS_DIR" ]]; then
+ echo "Error: Questions directory not found: $QUESTIONS_DIR" >&2
+ echo "Run split_questions.py first to generate per-question files." >&2
+ exit 1
+fi
+
+# ── Helper: find question file by number ──────────────────────────────────────
+# Matches question number against filenames like pytanie_01.md, pytanie_13_27.md
+find_question_file() {
+ local num="$1"
+ local padded
+ padded=$(printf "%02d" "$num")
+
+ # Try exact match: pytanie_NN.md
+ local exact="${QUESTIONS_DIR}/pytanie_${padded}.md"
+ if [[ -f "$exact" ]]; then
+ echo "$exact"
+ return 0
+ fi
+
+ # Try dual-numbered: pytanie_NN_MM.md (match either side)
+ for f in "${QUESTIONS_DIR}"/pytanie_*_*.md; do
+ [[ -f "$f" ]] || continue
+ local base
+ base=$(basename "$f" .md)
+ # Extract numbers from pytanie_NN_MM
+ local nums="${base#pytanie_}"
+ local left="${nums%%_*}"
+ local right="${nums##*_}"
+ if [[ "$padded" == "$left" || "$padded" == "$right" ]]; then
+ echo "$f"
+ return 0
+ fi
+ done
+
+ return 1
+}
+
+# ── List questions ────────────────────────────────────────────────────────────
+list_questions() {
+ echo "Available questions in ${QUESTIONS_DIR}/"
+ echo ""
+ for f in "${QUESTIONS_DIR}"/pytanie_*.md; do
+ [[ -f "$f" ]] || continue
+ # Read first line to get the header
+ local header
+ header=$(head -1 "$f")
+ local num title
+ num=$(echo "$header" | sed 's/^## PYTANIE \([0-9/]*\):.*/\1/')
+ title=$(echo "$header" | sed 's/^## PYTANIE [0-9/]*: //')
+ printf " %6s %s\n" "$num" "$title"
+ done
+ echo ""
+ echo "Total: $(ls -1 "${QUESTIONS_DIR}"/pytanie_*.md 2>/dev/null | wc -l) questions"
+}
+
+if $LIST_ONLY; then
+ list_questions
+ exit 0
+fi
+
+# ── Assemble selected questions ───────────────────────────────────────────────
+assemble_questions() {
+ local selected=("$@")
+
+ if [[ ${#selected[@]} -eq 0 ]]; then
+ # All questions — concatenate all files in order
+ local first=true
+ for f in "${QUESTIONS_DIR}"/pytanie_*.md; do
+ [[ -f "$f" ]] || continue
+ if ! $first; then
+ echo ""
+ echo "\\newpage"
+ echo ""
+ fi
+ first=false
+ cat "$f"
+ done
+ return
+ fi
+
+ # Selected questions
+ local first=true
+ local found_any=false
+ for sel in "${selected[@]}"; do
+ local qfile
+ if qfile=$(find_question_file "$sel"); then
+ found_any=true
+ if ! $first; then
+ echo ""
+ echo "\\newpage"
+ echo ""
+ fi
+ first=false
+ cat "$qfile"
+ else
+ echo "Warning: Question $sel not found, skipping." >&2
+ fi
+ done
+
+ if ! $found_any; then
+ return 1
+ fi
+}
+
+# ── Generate output filename ─────────────────────────────────────────────────
+if [[ -z "$OUTPUT_PDF" ]]; then
+ if [[ ${#QUESTIONS[@]} -eq 0 ]]; then
+ OUTPUT_PDF="/tmp/obrona_all_questions.pdf"
+ elif [[ ${#QUESTIONS[@]} -eq 1 ]]; then
+ OUTPUT_PDF="/tmp/obrona_q${QUESTIONS[0]}.pdf"
+ else
+ joined=$(IFS=_; echo "${QUESTIONS[*]}")
+ joined="${joined//\//-}" # Replace / with - for safe filenames
+ OUTPUT_PDF="/tmp/obrona_q${joined}.pdf"
+ fi
+fi
+
+# ── Create temporary markdown ────────────────────────────────────────────────
+TMP_DIR=$(mktemp -d)
+TMP_MD="${TMP_DIR}/questions.md"
+
+if ! assemble_questions "${QUESTIONS[@]}" > "$TMP_MD"; then
+ echo "Error: No matching questions found for: ${QUESTIONS[*]}" >&2
+ echo "Use --list to see available questions." >&2
+ rm -rf "$TMP_DIR"
+ exit 1
+fi
+
+# Count extracted questions
+extracted=$(grep -c "^## PYTANIE" "$TMP_MD" || echo "0")
+if [[ "$extracted" -eq 0 ]]; then
+ echo "Error: No matching questions found for: ${QUESTIONS[*]}" >&2
+ echo "Use --list to see available questions." >&2
+ rm -rf "$TMP_DIR"
+ exit 1
+fi
+
+# ── Convert to PDF via pandoc + xelatex ───────────────────────────────────────
+echo "Converting $extracted question(s) to PDF..."
+
+pandoc "$TMP_MD" \
+ -o "$OUTPUT_PDF" \
+ --pdf-engine=xelatex \
+ --resource-path="${SCRIPT_DIR}" \
+ -V geometry:a4paper \
+ -V geometry:margin=1.8cm \
+ -V fontsize=12pt \
+ -V mainfont="DejaVu Sans" \
+ -V sansfont="DejaVu Sans" \
+ -V monofont="DejaVu Sans Mono" \
+ -V linestretch=1.15 \
+ -V colorlinks=false \
+ --highlight-style=monochrome \
+ -V header-includes='\usepackage{fancyhdr}\pagestyle{fancy}\fancyhead[L]{\small Obrona magisterska}\fancyhead[R]{\small\thepage}\fancyfoot{}' \
+ -V header-includes='\usepackage{enumitem}\setlist{nosep,leftmargin=*}' \
+ -V header-includes='\renewcommand{\arraystretch}{1.3}' \
+ -V header-includes='\usepackage{booktabs}' \
+ 2>/dev/null
+
+echo "PDF generated: $OUTPUT_PDF"
+echo " Pages: $(pdfinfo "$OUTPUT_PDF" 2>/dev/null | grep Pages | awk '{print $2}' || echo "?")"
+
+# ── Print ─────────────────────────────────────────────────────────────────────
+if $DO_PRINT; then
+ if ! lpstat -p "$PRINTER" &>/dev/null; then
+ echo "Error: Printer '$PRINTER' not found." >&2
+ echo "Available printers:" >&2
+ lpstat -p 2>/dev/null | awk '{print " " $2}' >&2
+ rm -rf "$TMP_DIR"
+ exit 1
+ fi
+
+ echo "Printing to $PRINTER..."
+ lp -d "$PRINTER" \
+ -o media=A4 \
+ -o sides=one-sided \
+ -o fit-to-page \
+ "$OUTPUT_PDF"
+ echo "Print job submitted."
+fi
+
+# ── Cleanup ───────────────────────────────────────────────────────────────────
+if $KEEP_TMP; then
+ echo "Temporary files kept in: $TMP_DIR"
+else
+ rm -rf "$TMP_DIR"
+fi
diff --git a/python_pkg/praca_magisterska_video/generate_images/split_questions.py b/python_pkg/praca_magisterska_video/generate_images/split_questions.py
new file mode 100755
index 0000000..c84dc02
--- /dev/null
+++ b/python_pkg/praca_magisterska_video/generate_images/split_questions.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+"""Split OBRONA_MAGISTERSKA_ODPOWIEDZI.md into per-question files.
+
+Each file: pytanie_NN.md (or pytanie_NN_MM.md for dual-numbered like 13/27).
+Placed in pytania/questions/ folder.
+"""
+
+from pathlib import Path
+import re
+
+SCRIPT_DIR = str(Path(__file__).resolve().parent)
+SOURCE = str(Path(SCRIPT_DIR) / "OBRONA_MAGISTERSKA_ODPOWIEDZI.md")
+OUT_DIR = str(Path(SCRIPT_DIR) / "questions")
+Path(OUT_DIR).mkdir(parents=True, exist_ok=True)
+
+with Path(SOURCE).open(encoding="utf-8") as f:
+ lines = f.readlines()
+
+# Find all question boundaries
+question_starts = []
+for i, line in enumerate(lines):
+ m = re.match(r"^## PYTANIE (\d+(/\d+)?):(.*)$", line)
+ if m:
+ raw_num = m.group(1) # e.g. "13/27" or "1"
+ title = m.group(3).strip()
+ question_starts.append((i, raw_num, title))
+
+print(f"Found {len(question_starts)} questions")
+
+for idx, (start_line, raw_num, title) in enumerate(question_starts):
+ # End = next question start or EOF
+ if idx + 1 < len(question_starts):
+ end_line = question_starts[idx + 1][0]
+ else:
+ end_line = len(lines)
+
+ # Extract content, strip trailing \newpage and blank lines
+ content_lines = lines[start_line:end_line]
+ # Remove trailing \newpage and blank lines
+ while content_lines and content_lines[-1].strip() in ("", "\\newpage"):
+ content_lines.pop()
+ # Add final newline
+ content_lines.append("\n")
+
+ # Build filename: pytanie_01.md, pytanie_13_27.md
+ safe_num = raw_num.replace("/", "_")
+ # Zero-pad single numbers for sorting
+ parts = safe_num.split("_")
+ padded = "_".join(p.zfill(2) for p in parts)
+ filename = f"pytanie_{padded}.md"
+
+ filepath = str(Path(OUT_DIR) / filename)
+ with Path(filepath).open("w", encoding="utf-8") as f:
+ f.writelines(content_lines)
+
+ line_count = len(content_lines)
+ print(f" {filename:30s} ({line_count:4d} lines) PYTANIE {raw_num}: {title}")
+
+print(f"\nAll files written to: {OUT_DIR}")
diff --git a/python_pkg/praca_magisterska_video/images/img/4plus1_view_model.png b/python_pkg/praca_magisterska_video/images/img/4plus1_view_model.png
new file mode 100644
index 0000000..6ab3ea4
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/4plus1_view_model.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/agent_3t_architecture.png b/python_pkg/praca_magisterska_video/images/img/agent_3t_architecture.png
new file mode 100644
index 0000000..edb25c1
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/agent_3t_architecture.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/agent_bdi_model.png b/python_pkg/praca_magisterska_video/images/img/agent_bdi_model.png
new file mode 100644
index 0000000..7eb2f2a
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/agent_bdi_model.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/agent_behavior_tree.png b/python_pkg/praca_magisterska_video/images/img/agent_behavior_tree.png
new file mode 100644
index 0000000..d2dff09
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/agent_behavior_tree.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/agent_see_think_act.png b/python_pkg/praca_magisterska_video/images/img/agent_see_think_act.png
new file mode 100644
index 0000000..257d326
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/agent_see_think_act.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/archimate_layers.png b/python_pkg/praca_magisterska_video/images/img/archimate_layers.png
new file mode 100644
index 0000000..661d8f8
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/archimate_layers.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/astar_traversal.png b/python_pkg/praca_magisterska_video/images/img/astar_traversal.png
new file mode 100644
index 0000000..5e0b4c1
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/astar_traversal.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/bellman_ford_negative_cycle.png b/python_pkg/praca_magisterska_video/images/img/bellman_ford_negative_cycle.png
new file mode 100644
index 0000000..fc4035b
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/bellman_ford_negative_cycle.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/bellman_ford_negative_weights.png b/python_pkg/praca_magisterska_video/images/img/bellman_ford_negative_weights.png
new file mode 100644
index 0000000..2bf072b
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/bellman_ford_negative_weights.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/bellman_ford_traversal.png b/python_pkg/praca_magisterska_video/images/img/bellman_ford_traversal.png
new file mode 100644
index 0000000..c15fefe
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/bellman_ford_traversal.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/bpmn_reklamacja.png b/python_pkg/praca_magisterska_video/images/img/bpmn_reklamacja.png
new file mode 100644
index 0000000..6ea67ca
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/bpmn_reklamacja.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/c4_model.png b/python_pkg/praca_magisterska_video/images/img/c4_model.png
new file mode 100644
index 0000000..807458e
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/c4_model.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/cpm_example.png b/python_pkg/praca_magisterska_video/images/img/cpm_example.png
new file mode 100644
index 0000000..fe91a2b
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/cpm_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/deadlock_illustration.png b/python_pkg/praca_magisterska_video/images/img/deadlock_illustration.png
new file mode 100644
index 0000000..e94ce84
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/deadlock_illustration.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/dijkstra_traversal.png b/python_pkg/praca_magisterska_video/images/img/dijkstra_traversal.png
new file mode 100644
index 0000000..2540c6b
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/dijkstra_traversal.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/epc_reklamacja.png b/python_pkg/praca_magisterska_video/images/img/epc_reklamacja.png
new file mode 100644
index 0000000..a11236c
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/epc_reklamacja.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/fa_recognition_example.png b/python_pkg/praca_magisterska_video/images/img/fa_recognition_example.png
new file mode 100644
index 0000000..51d1721
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/fa_recognition_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/flowchart_reklamacja.png b/python_pkg/praca_magisterska_video/images/img/flowchart_reklamacja.png
new file mode 100644
index 0000000..13db1c7
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/flowchart_reklamacja.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/ford_fulkerson_example.png b/python_pkg/praca_magisterska_video/images/img/ford_fulkerson_example.png
new file mode 100644
index 0000000..7c1d56f
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/ford_fulkerson_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/fsd_ssd_comparison.png b/python_pkg/praca_magisterska_video/images/img/fsd_ssd_comparison.png
new file mode 100644
index 0000000..e72718c
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/fsd_ssd_comparison.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/graph_example_structure.png b/python_pkg/praca_magisterska_video/images/img/graph_example_structure.png
new file mode 100644
index 0000000..2995f28
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/graph_example_structure.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/hog_svm_pipeline.png b/python_pkg/praca_magisterska_video/images/img/hog_svm_pipeline.png
new file mode 100644
index 0000000..811ef09
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/hog_svm_pipeline.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/hungarian_example.png b/python_pkg/praca_magisterska_video/images/img/hungarian_example.png
new file mode 100644
index 0000000..4547185
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/hungarian_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/ipc_mechanisms.png b/python_pkg/praca_magisterska_video/images/img/ipc_mechanisms.png
new file mode 100644
index 0000000..6a723be
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/ipc_mechanisms.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/kruskal_example.png b/python_pkg/praca_magisterska_video/images/img/kruskal_example.png
new file mode 100644
index 0000000..4918862
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/kruskal_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/lba_recognition_example.png b/python_pkg/praca_magisterska_video/images/img/lba_recognition_example.png
new file mode 100644
index 0000000..3f11a14
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/lba_recognition_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/linearizability_vs_sequential.png b/python_pkg/praca_magisterska_video/images/img/linearizability_vs_sequential.png
new file mode 100644
index 0000000..b18ff88
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/linearizability_vs_sequential.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/min_cost_flow_example.png b/python_pkg/praca_magisterska_video/images/img/min_cost_flow_example.png
new file mode 100644
index 0000000..e85d255
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/min_cost_flow_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/network_models_mnemonic.png b/python_pkg/praca_magisterska_video/images/img/network_models_mnemonic.png
new file mode 100644
index 0000000..cae297c
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/network_models_mnemonic.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/nf_0nf_table.png b/python_pkg/praca_magisterska_video/images/img/nf_0nf_table.png
new file mode 100644
index 0000000..ac8bbd1
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/nf_0nf_table.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/nf_1nf_tables.png b/python_pkg/praca_magisterska_video/images/img/nf_1nf_tables.png
new file mode 100644
index 0000000..e6eab68
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/nf_1nf_tables.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/nf_2nf_tables.png b/python_pkg/praca_magisterska_video/images/img/nf_2nf_tables.png
new file mode 100644
index 0000000..751aa6f
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/nf_2nf_tables.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/nf_3nf_tables.png b/python_pkg/praca_magisterska_video/images/img/nf_3nf_tables.png
new file mode 100644
index 0000000..0ff6858
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/nf_3nf_tables.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/nf_4nf_example.png b/python_pkg/praca_magisterska_video/images/img/nf_4nf_example.png
new file mode 100644
index 0000000..98fae7f
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/nf_4nf_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/nf_5nf_example.png b/python_pkg/praca_magisterska_video/images/img/nf_5nf_example.png
new file mode 100644
index 0000000..d8f218a
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/nf_5nf_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/nf_bcnf_tables.png b/python_pkg/praca_magisterska_video/images/img/nf_bcnf_tables.png
new file mode 100644
index 0000000..49d9714
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/nf_bcnf_tables.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/nf_summary_flow.png b/python_pkg/praca_magisterska_video/images/img/nf_summary_flow.png
new file mode 100644
index 0000000..e36a997
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/nf_summary_flow.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/paxos_flow.png b/python_pkg/praca_magisterska_video/images/img/paxos_flow.png
new file mode 100644
index 0000000..d424b10
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/paxos_flow.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/pda_recognition_example.png b/python_pkg/praca_magisterska_video/images/img/pda_recognition_example.png
new file mode 100644
index 0000000..8c7da4a
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/pda_recognition_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/producer_consumer.png b/python_pkg/praca_magisterska_video/images/img/producer_consumer.png
new file mode 100644
index 0000000..5190202
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/producer_consumer.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/pubsub_delivery_guarantees.png b/python_pkg/praca_magisterska_video/images/img/pubsub_delivery_guarantees.png
new file mode 100644
index 0000000..356b7ca
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/pubsub_delivery_guarantees.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/pubsub_qos_at_least_once.png b/python_pkg/praca_magisterska_video/images/img/pubsub_qos_at_least_once.png
new file mode 100644
index 0000000..7a6077d
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/pubsub_qos_at_least_once.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/pubsub_qos_at_most_once.png b/python_pkg/praca_magisterska_video/images/img/pubsub_qos_at_most_once.png
new file mode 100644
index 0000000..aba7436
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/pubsub_qos_at_most_once.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/pubsub_qos_exactly_once.png b/python_pkg/praca_magisterska_video/images/img/pubsub_qos_exactly_once.png
new file mode 100644
index 0000000..fb3e0e4
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/pubsub_qos_exactly_once.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/pubsub_sub_content.png b/python_pkg/praca_magisterska_video/images/img/pubsub_sub_content.png
new file mode 100644
index 0000000..5f439dc
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/pubsub_sub_content.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/pubsub_sub_hierarchical.png b/python_pkg/praca_magisterska_video/images/img/pubsub_sub_hierarchical.png
new file mode 100644
index 0000000..fc393c7
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/pubsub_sub_hierarchical.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/pubsub_sub_topic.png b/python_pkg/praca_magisterska_video/images/img/pubsub_sub_topic.png
new file mode 100644
index 0000000..1306afe
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/pubsub_sub_topic.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/pubsub_sub_type.png b/python_pkg/praca_magisterska_video/images/img/pubsub_sub_type.png
new file mode 100644
index 0000000..2db8b51
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/pubsub_sub_type.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/pubsub_subscription_types.png b/python_pkg/praca_magisterska_video/images/img/pubsub_subscription_types.png
new file mode 100644
index 0000000..7444931
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/pubsub_subscription_types.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q14_catalog_map.png b/python_pkg/praca_magisterska_video/images/img/q14_catalog_map.png
new file mode 100644
index 0000000..b269481
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q14_catalog_map.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q14_observer_card_filled.png b/python_pkg/praca_magisterska_video/images/img/q14_observer_card_filled.png
new file mode 100644
index 0000000..36a2501
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q14_observer_card_filled.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q14_pattern_language_navigation.png b/python_pkg/praca_magisterska_video/images/img/q14_pattern_language_navigation.png
new file mode 100644
index 0000000..26bfaba
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q14_pattern_language_navigation.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q14_pattern_template.png b/python_pkg/praca_magisterska_video/images/img/q14_pattern_template.png
new file mode 100644
index 0000000..9f787ae
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q14_pattern_template.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q14_three_pillars.png b/python_pkg/praca_magisterska_video/images/img/q14_three_pillars.png
new file mode 100644
index 0000000..71e4d97
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q14_three_pillars.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_batch_vs_streaming.png b/python_pkg/praca_magisterska_video/images/img/q20_batch_vs_streaming.png
new file mode 100644
index 0000000..ed1f98f
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_batch_vs_streaming.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_decision_tree.png b/python_pkg/praca_magisterska_video/images/img/q20_decision_tree.png
new file mode 100644
index 0000000..8c0e0ed
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_decision_tree.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_event_vs_processing_time.png b/python_pkg/praca_magisterska_video/images/img/q20_event_vs_processing_time.png
new file mode 100644
index 0000000..20f4f39
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_event_vs_processing_time.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_exactly_once.png b/python_pkg/praca_magisterska_video/images/img/q20_exactly_once.png
new file mode 100644
index 0000000..14f64e1
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_exactly_once.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_flink_arch.png b/python_pkg/praca_magisterska_video/images/img/q20_flink_arch.png
new file mode 100644
index 0000000..ecf5a0b
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_flink_arch.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_kafka_streams_arch.png b/python_pkg/praca_magisterska_video/images/img/q20_kafka_streams_arch.png
new file mode 100644
index 0000000..c2c6544
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_kafka_streams_arch.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_lambda_kappa_table.png b/python_pkg/praca_magisterska_video/images/img/q20_lambda_kappa_table.png
new file mode 100644
index 0000000..011cdc5
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_lambda_kappa_table.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_lambda_vs_kappa.png b/python_pkg/praca_magisterska_video/images/img/q20_lambda_vs_kappa.png
new file mode 100644
index 0000000..0ce68db
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_lambda_vs_kappa.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_late_data_strategies.png b/python_pkg/praca_magisterska_video/images/img/q20_late_data_strategies.png
new file mode 100644
index 0000000..7c60548
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_late_data_strategies.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_platform_comparison.png b/python_pkg/praca_magisterska_video/images/img/q20_platform_comparison.png
new file mode 100644
index 0000000..f9b29b4
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_platform_comparison.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_session_users.png b/python_pkg/praca_magisterska_video/images/img/q20_session_users.png
new file mode 100644
index 0000000..cf33e91
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_session_users.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_sliding_sla.png b/python_pkg/praca_magisterska_video/images/img/q20_sliding_sla.png
new file mode 100644
index 0000000..2d882d9
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_sliding_sla.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_spark_streaming_arch.png b/python_pkg/praca_magisterska_video/images/img/q20_spark_streaming_arch.png
new file mode 100644
index 0000000..eb67d36
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_spark_streaming_arch.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_streaming_ecosystem.png b/python_pkg/praca_magisterska_video/images/img/q20_streaming_ecosystem.png
new file mode 100644
index 0000000..6c364d4
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_streaming_ecosystem.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_true_vs_microbatch.png b/python_pkg/praca_magisterska_video/images/img/q20_true_vs_microbatch.png
new file mode 100644
index 0000000..eed6744
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_true_vs_microbatch.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_tumbling_fraud.png b/python_pkg/praca_magisterska_video/images/img/q20_tumbling_fraud.png
new file mode 100644
index 0000000..164a89f
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_tumbling_fraud.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q20_window_types.png b/python_pkg/praca_magisterska_video/images/img/q20_window_types.png
new file mode 100644
index 0000000..1d78b29
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q20_window_types.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_diy_thresholding.png b/python_pkg/praca_magisterska_video/images/img/q23_diy_thresholding.png
new file mode 100644
index 0000000..154575d
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_diy_thresholding.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_diy_unet.png b/python_pkg/praca_magisterska_video/images/img/q23_diy_unet.png
new file mode 100644
index 0000000..2a69790
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_diy_unet.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_dot_product.png b/python_pkg/praca_magisterska_video/images/img/q23_dot_product.png
new file mode 100644
index 0000000..b0eadba
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_dot_product.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_fc_vs_conv1x1.png b/python_pkg/praca_magisterska_video/images/img/q23_fc_vs_conv1x1.png
new file mode 100644
index 0000000..6ae3326
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_fc_vs_conv1x1.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_mean_shift.png b/python_pkg/praca_magisterska_video/images/img/q23_mean_shift.png
new file mode 100644
index 0000000..eae1317
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_mean_shift.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_mnemonics.png b/python_pkg/praca_magisterska_video/images/img/q23_mnemonics.png
new file mode 100644
index 0000000..6e833b9
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_mnemonics.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_normalized_cuts.png b/python_pkg/praca_magisterska_video/images/img/q23_normalized_cuts.png
new file mode 100644
index 0000000..bf2a908
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_normalized_cuts.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_otsu_bimodal.png b/python_pkg/praca_magisterska_video/images/img/q23_otsu_bimodal.png
new file mode 100644
index 0000000..4420c3d
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_otsu_bimodal.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_receptive_field.png b/python_pkg/praca_magisterska_video/images/img/q23_receptive_field.png
new file mode 100644
index 0000000..2b02bf3
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_receptive_field.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_region_growing.png b/python_pkg/praca_magisterska_video/images/img/q23_region_growing.png
new file mode 100644
index 0000000..91c363c
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_region_growing.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_relu.png b/python_pkg/praca_magisterska_video/images/img/q23_relu.png
new file mode 100644
index 0000000..5a5dc27
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_relu.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_transformer_attention.png b/python_pkg/praca_magisterska_video/images/img/q23_transformer_attention.png
new file mode 100644
index 0000000..ee78d2f
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_transformer_attention.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_unet_arch.png b/python_pkg/praca_magisterska_video/images/img/q23_unet_arch.png
new file mode 100644
index 0000000..4dcc888
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_unet_arch.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q23_watershed.png b/python_pkg/praca_magisterska_video/images/img/q23_watershed.png
new file mode 100644
index 0000000..4b58fed
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q23_watershed.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_anchor_boxes.png b/python_pkg/praca_magisterska_video/images/img/q24_anchor_boxes.png
new file mode 100644
index 0000000..21458df
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_anchor_boxes.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_cnn_architecture.png b/python_pkg/praca_magisterska_video/images/img/q24_cnn_architecture.png
new file mode 100644
index 0000000..5f35b38
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_cnn_architecture.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_detection_tasks.png b/python_pkg/praca_magisterska_video/images/img/q24_detection_tasks.png
new file mode 100644
index 0000000..34cdaa4
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_detection_tasks.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_detector_from_classifier.png b/python_pkg/praca_magisterska_video/images/img/q24_detector_from_classifier.png
new file mode 100644
index 0000000..efa805c
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_detector_from_classifier.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_detr_pipeline.png b/python_pkg/praca_magisterska_video/images/img/q24_detr_pipeline.png
new file mode 100644
index 0000000..02de5ed
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_detr_pipeline.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_fpn.png b/python_pkg/praca_magisterska_video/images/img/q24_fpn.png
new file mode 100644
index 0000000..c57fbd2
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_fpn.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_haar_features.png b/python_pkg/praca_magisterska_video/images/img/q24_haar_features.png
new file mode 100644
index 0000000..f4f67b4
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_haar_features.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_hog_gradient_steps.png b/python_pkg/praca_magisterska_video/images/img/q24_hog_gradient_steps.png
new file mode 100644
index 0000000..dda2b6a
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_hog_gradient_steps.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_hog_svm_pipeline.png b/python_pkg/praca_magisterska_video/images/img/q24_hog_svm_pipeline.png
new file mode 100644
index 0000000..59f00f4
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_hog_svm_pipeline.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_integral_image.png b/python_pkg/praca_magisterska_video/images/img/q24_integral_image.png
new file mode 100644
index 0000000..437d408
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_integral_image.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_iou_diagram.png b/python_pkg/praca_magisterska_video/images/img/q24_iou_diagram.png
new file mode 100644
index 0000000..69bcb45
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_iou_diagram.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_nms_steps.png b/python_pkg/praca_magisterska_video/images/img/q24_nms_steps.png
new file mode 100644
index 0000000..7f6bd8c
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_nms_steps.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_rcnn_evolution.png b/python_pkg/praca_magisterska_video/images/img/q24_rcnn_evolution.png
new file mode 100644
index 0000000..fcecb56
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_rcnn_evolution.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_roi_pooling.png b/python_pkg/praca_magisterska_video/images/img/q24_roi_pooling.png
new file mode 100644
index 0000000..8515ab6
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_roi_pooling.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_sliding_window.png b/python_pkg/praca_magisterska_video/images/img/q24_sliding_window.png
new file mode 100644
index 0000000..fef8642
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_sliding_window.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_svm_hyperplane.png b/python_pkg/praca_magisterska_video/images/img/q24_svm_hyperplane.png
new file mode 100644
index 0000000..aec786c
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_svm_hyperplane.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_two_vs_one_stage.png b/python_pkg/praca_magisterska_video/images/img/q24_two_vs_one_stage.png
new file mode 100644
index 0000000..d5fda43
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_two_vs_one_stage.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_viola_jones_cascade.png b/python_pkg/praca_magisterska_video/images/img/q24_viola_jones_cascade.png
new file mode 100644
index 0000000..66882d0
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_viola_jones_cascade.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q24_yolo_grid.png b/python_pkg/praca_magisterska_video/images/img/q24_yolo_grid.png
new file mode 100644
index 0000000..a8480d4
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q24_yolo_grid.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q31_conditions_spectrum.png b/python_pkg/praca_magisterska_video/images/img/q31_conditions_spectrum.png
new file mode 100644
index 0000000..11ea625
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q31_conditions_spectrum.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q31_criteria_comparison.png b/python_pkg/praca_magisterska_video/images/img/q31_criteria_comparison.png
new file mode 100644
index 0000000..0e305db
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q31_criteria_comparison.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q31_criteria_mnemonic.png b/python_pkg/praca_magisterska_video/images/img/q31_criteria_mnemonic.png
new file mode 100644
index 0000000..866c68a
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q31_criteria_mnemonic.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q31_expected_value.png b/python_pkg/praca_magisterska_video/images/img/q31_expected_value.png
new file mode 100644
index 0000000..dd353b1
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q31_expected_value.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q31_hurwicz_alpha.png b/python_pkg/praca_magisterska_video/images/img/q31_hurwicz_alpha.png
new file mode 100644
index 0000000..ffd14f2
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q31_hurwicz_alpha.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q31_regret_matrix.png b/python_pkg/praca_magisterska_video/images/img/q31_regret_matrix.png
new file mode 100644
index 0000000..b78f4ba
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q31_regret_matrix.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_classic_problems.png b/python_pkg/praca_magisterska_video/images/img/q9_classic_problems.png
new file mode 100644
index 0000000..e1aeaaa
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_classic_problems.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_coffman_strategies.png b/python_pkg/praca_magisterska_video/images/img/q9_coffman_strategies.png
new file mode 100644
index 0000000..ef55e3e
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_coffman_strategies.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_deadlock_scenario.png b/python_pkg/praca_magisterska_video/images/img/q9_deadlock_scenario.png
new file mode 100644
index 0000000..1c69613
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_deadlock_scenario.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_ipc_details.png b/python_pkg/praca_magisterska_video/images/img/q9_ipc_details.png
new file mode 100644
index 0000000..a1d0b44
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_ipc_details.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_ipc_table.png b/python_pkg/praca_magisterska_video/images/img/q9_ipc_table.png
new file mode 100644
index 0000000..54a85d3
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_ipc_table.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_memory_layout.png b/python_pkg/praca_magisterska_video/images/img/q9_memory_layout.png
new file mode 100644
index 0000000..752a033
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_memory_layout.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_pcb_structure.png b/python_pkg/praca_magisterska_video/images/img/q9_pcb_structure.png
new file mode 100644
index 0000000..311c6e4
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_pcb_structure.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_process_states.png b/python_pkg/praca_magisterska_video/images/img/q9_process_states.png
new file mode 100644
index 0000000..fd187a7
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_process_states.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_process_vs_thread.png b/python_pkg/praca_magisterska_video/images/img/q9_process_vs_thread.png
new file mode 100644
index 0000000..2d5425d
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_process_vs_thread.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_race_condition.png b/python_pkg/praca_magisterska_video/images/img/q9_race_condition.png
new file mode 100644
index 0000000..f041dde
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_race_condition.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_scenario_table.png b/python_pkg/praca_magisterska_video/images/img/q9_scenario_table.png
new file mode 100644
index 0000000..229176b
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_scenario_table.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_semaphore_concept.png b/python_pkg/praca_magisterska_video/images/img/q9_semaphore_concept.png
new file mode 100644
index 0000000..bdb2d38
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_semaphore_concept.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_speed_comparison.png b/python_pkg/praca_magisterska_video/images/img/q9_speed_comparison.png
new file mode 100644
index 0000000..fd846f8
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_speed_comparison.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_starvation_priority.png b/python_pkg/praca_magisterska_video/images/img/q9_starvation_priority.png
new file mode 100644
index 0000000..8bf4a60
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_starvation_priority.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_sync_comparison.png b/python_pkg/praca_magisterska_video/images/img/q9_sync_comparison.png
new file mode 100644
index 0000000..6cfaabd
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_sync_comparison.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/q9_thread_structure.png b/python_pkg/praca_magisterska_video/images/img/q9_thread_structure.png
new file mode 100644
index 0000000..d00cddd
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/q9_thread_structure.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/rcnn_evolution.png b/python_pkg/praca_magisterska_video/images/img/rcnn_evolution.png
new file mode 100644
index 0000000..5e113b5
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/rcnn_evolution.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/robot_movement_types.png b/python_pkg/praca_magisterska_video/images/img/robot_movement_types.png
new file mode 100644
index 0000000..f1e8604
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/robot_movement_types.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/robot_online_offline.png b/python_pkg/praca_magisterska_video/images/img/robot_online_offline.png
new file mode 100644
index 0000000..c2548c0
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/robot_online_offline.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/robot_rapid_example.png b/python_pkg/praca_magisterska_video/images/img/robot_rapid_example.png
new file mode 100644
index 0000000..a7c27a5
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/robot_rapid_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/robot_ros_architecture.png b/python_pkg/praca_magisterska_video/images/img/robot_ros_architecture.png
new file mode 100644
index 0000000..65a96a1
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/robot_ros_architecture.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/robot_trms_pyramid.png b/python_pkg/praca_magisterska_video/images/img/robot_trms_pyramid.png
new file mode 100644
index 0000000..c07f16a
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/robot_trms_pyramid.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/robot_vendor_comparison.png b/python_pkg/praca_magisterska_video/images/img/robot_vendor_comparison.png
new file mode 100644
index 0000000..2e17455
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/robot_vendor_comparison.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/scheduling_complexity_map.png b/python_pkg/praca_magisterska_video/images/img/scheduling_complexity_map.png
new file mode 100644
index 0000000..5d0a360
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/scheduling_complexity_map.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/scheduling_edd_example.png b/python_pkg/praca_magisterska_video/images/img/scheduling_edd_example.png
new file mode 100644
index 0000000..cf7b8a7
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/scheduling_edd_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/scheduling_flow_vs_job.png b/python_pkg/praca_magisterska_video/images/img/scheduling_flow_vs_job.png
new file mode 100644
index 0000000..0e54c4b
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/scheduling_flow_vs_job.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/scheduling_graham_notation.png b/python_pkg/praca_magisterska_video/images/img/scheduling_graham_notation.png
new file mode 100644
index 0000000..b8efdae
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/scheduling_graham_notation.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/scheduling_johnson_gantt.png b/python_pkg/praca_magisterska_video/images/img/scheduling_johnson_gantt.png
new file mode 100644
index 0000000..4ddebdc
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/scheduling_johnson_gantt.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/scheduling_spt_comparison.png b/python_pkg/praca_magisterska_video/images/img/scheduling_spt_comparison.png
new file mode 100644
index 0000000..c34b1e6
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/scheduling_spt_comparison.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/segmentation_types.png b/python_pkg/praca_magisterska_video/images/img/segmentation_types.png
new file mode 100644
index 0000000..f3eaa7d
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/segmentation_types.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/tm_recognition_example.png b/python_pkg/praca_magisterska_video/images/img/tm_recognition_example.png
new file mode 100644
index 0000000..a225fe4
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/tm_recognition_example.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/togaf_adm.png b/python_pkg/praca_magisterska_video/images/img/togaf_adm.png
new file mode 100644
index 0000000..69fffa2
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/togaf_adm.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/tsp_nearest_neighbor.png b/python_pkg/praca_magisterska_video/images/img/tsp_nearest_neighbor.png
new file mode 100644
index 0000000..540eeac
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/tsp_nearest_neighbor.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/uml_activity_reklamacja.png b/python_pkg/praca_magisterska_video/images/img/uml_activity_reklamacja.png
new file mode 100644
index 0000000..898fed8
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/uml_activity_reklamacja.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/vector_clock_timeline.png b/python_pkg/praca_magisterska_video/images/img/vector_clock_timeline.png
new file mode 100644
index 0000000..7a37b2c
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/vector_clock_timeline.png differ
diff --git a/python_pkg/praca_magisterska_video/images/img/zachman_framework.png b/python_pkg/praca_magisterska_video/images/img/zachman_framework.png
new file mode 100644
index 0000000..57a902f
Binary files /dev/null and b/python_pkg/praca_magisterska_video/images/img/zachman_framework.png differ