diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9557dd..1ae19cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -96,7 +96,7 @@ repos: - types-requests - types-PyYAML - types-python-dateutil - exclude: ^(Bash/|\.venv/|python_pkg/music_gen/) + exclude: ^(Bash/|\.venv/|python_pkg/music_gen/|python_pkg/praca_magisterska_video/) # =========================================================================== # PYLINT - Comprehensive Python linter @@ -162,7 +162,7 @@ repos: - id: codespell args: - --skip=*.json,*.lock,*.min.js,*.min.css,.git,__pycache__,.venv,*.txt - - --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile + - --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,proces,serwer,parametr,adres,hart,dout exclude: ^(Bash/ffmpeg-build/|LaTeX/|CPP/|.*\.geojson$) # =========================================================================== diff --git a/pyproject.toml b/pyproject.toml index e1f582a..e39d597 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -181,6 +181,28 @@ unfixable = [] "C416", # Unnecessary list comprehension - legacy code "E501", # Line too long - legacy code ] +# Thesis diagram generators - one-off matplotlib plotting scripts +"python_pkg/praca_magisterska_video/**/*.py" = [ + "ANN001", # Missing type annotation - plotting scripts, not library code + "ANN202", # Missing return type (private) + "BLE001", # Blind exception (file I/O error handling) + "C901", # Function complexity - long plotting functions + "E501", # Line too long - matplotlib call chains + "E741", # Ambiguous variable name - math convention (l, I) + "ERA001", # Commented-out code - contains notes and alternatives + "FBT002", # Boolean default argument - matplotlib patterns + "FBT003", # Boolean positional value - matplotlib API calls + "N806", # Uppercase variable - math convention (X, Y, F_A) + "PERF203", # try-except in loop - file processing + "PERF401", # List append in loop + "PLR0912", # Too many branches - plotting functions + "PLR0913", # Too many arguments - plotting functions + "PLR0915", # Too many statements - plotting functions + "PLR2004", # Magic values - plot coordinates and sizes + "PLW2901", # Loop variable overwritten - data processing + "S110", # try-except-pass - optional dependency handling + "T201", # print() for script output +] [tool.ruff.lint.pydocstyle] convention = "google" # Use Google docstring convention @@ -241,6 +263,7 @@ exclude = [ "Bash/ffmpeg-build/", ".venv/", "python_pkg/music_gen/", # Uses dynamic imports from transformers + "python_pkg/praca_magisterska_video/", # Thesis plotting scripts ] # ============================================================================ 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: + +![Graf przykładowy — 4 wierzchołki z wagami](img/graph_example_structure.png) + + 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ą + +![Przejście grafu algorytmem Dijkstry — krok po kroku](img/dijkstra_traversal.png) + +**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 + +![Bellman-Ford — ujemne wagi vs Dijkstra](img/bellman_ford_negative_weights.png) + +![Bellman-Ford — wykrywanie cyklu ujemnego](img/bellman_ford_negative_cycle.png) + +![Przejście grafu algorytmem Bellmana-Forda — krok po kroku](img/bellman_ford_traversal.png) + +**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 + +![Przejście grafu algorytmem A* — krok po kroku](img/astar_traversal.png) + +--- + +### 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 = ( + "" + ) + 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 = ( + "" + ) + 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"" + 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 = "" + for aspect, value in items[:6]: + comparison_html += f"" + comparison_html += "
    AspektWartość
    {clean_text(aspect)}{clean_text(value)}
    " + + # 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 = ( + "" + ) + 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