feat: added scripts for generating images for praca magisterska obrona
268
python_pkg/praca_magisterska_video/answers/2/pytanie_02.md
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
## PYTANIE 2: Algorytmy najkrótszej ścieżki (AISDI)
|
||||||
|
|
||||||
|
**Omówić i porównać algorytmy: Dijkstry, Bellmana-Forda, A\*.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Tło pojęciowe — słowniczek
|
||||||
|
|
||||||
|
**Graf** — struktura danych składająca się z **wierzchołków** (vertices/nodes) połączonych **krawędziami** (edges). Np. mapa miast: miasta = wierzchołki, drogi = krawędzie.
|
||||||
|
|
||||||
|
**Wierzchołek (vertex, node)** — punkt w grafie. Oznaczany jako v, u, n, m itp. **V** = zbiór wszystkich wierzchołków; |V| = ich liczba.
|
||||||
|
|
||||||
|
**Krawędź (edge)** — połączenie między dwoma wierzchołkami. **E** = zbiór krawędzi; |E| = ich liczba. Krawędź może być skierowana (A→B ≠ B→A) lub nieskierowana (A↔B).
|
||||||
|
|
||||||
|
**Waga (weight)** — liczba przypisana do krawędzi, oznaczająca „koszt" przejścia. Np. odległość w km, czas podróży, opłata za przejazd. Graf z wagami = graf ważony.
|
||||||
|
|
||||||
|
**Koszt (cost)** — ogólne pojęcie „ceny" przejścia ścieżką. Koszt ścieżki = suma wag krawędzi na tej ścieżce. Cel algorytmów: znaleźć ścieżkę o **minimalnym koszcie**.
|
||||||
|
|
||||||
|
**SSSP (Single-Source Shortest Path)** — problem: mając JEDEN wierzchołek startowy (źródło), znajdź najkrótsze ścieżki do WSZYSTKICH pozostałych wierzchołków. Dijkstra i Bellman-Ford rozwiązują SSSP. **Single-Pair** — prostszy problem: znajdź najkrótszą ścieżkę z A do B (jednej konkretnej pary). A\* rozwiązuje Single-Pair.
|
||||||
|
|
||||||
|
**d[v]** — tablica odległości. **d** = tablica (array), **v** = wierzchołek. d[v] przechowuje aktualnie najlepsze znane oszacowanie odległości od źródła do wierzchołka v. Na początku d[start] = 0, d[wszystko inne] = ∞. Algorytm stopniowo poprawia te wartości.
|
||||||
|
|
||||||
|
**Zachłanny (greedy)** — strategia algorytmiczna: w każdym kroku wybierz opcję, która TERAZ wygląda najlepiej (lokalnie optymalna), bez cofania się. Dijkstra jest zachłanny: zawsze bierze wierzchołek o najmniejszym d[v] i nigdy go nie rewiduje.
|
||||||
|
|
||||||
|
**Relaksacja krawędzi (edge relaxation)** — kluczowa operacja. Sprawdza: „czy droga do v przez u jest krótsza niż dotychczas znana?" Jeśli d[u] + waga(u,v) < d[v], to zaktualizuj d[v]. Nazwa od „rozluźniania" — górne ograniczenie na odległość się „rozluźnia" (maleje) w stronę prawdziwej wartości.
|
||||||
|
|
||||||
|
**Tablica (array)** — najprostsza struktura danych: ciągły blok pamięci. W Dijkstrze z tablicą: szukanie minimum d[v] wymaga przejrzenia WSZYSTKICH wierzchołków → O(V) na szukanie × V razy = **O(V²)**.
|
||||||
|
|
||||||
|
Przykład — graf z 4 wierzchołkami (A, B, C, D), start = A:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
d = [ A:0, B:∞, C:∞, D:∞ ] ← tablica na starcie
|
||||||
|
odwiedzone = {}
|
||||||
|
|
||||||
|
Krok 1: przeszukaj CAŁĄ tablicę d → min = A (0)
|
||||||
|
d = [ A:0, B:2, C:4, D:∞ ] odw = {A}
|
||||||
|
↑ ↑
|
||||||
|
A→B=2 A→C=4 (relaksacja sąsiadów A)
|
||||||
|
|
||||||
|
Krok 2: przeszukaj CAŁĄ tablicę d (poza odw.) → min = B (2)
|
||||||
|
d = [ A:0, B:2, C:4, D:5 ] odw = {A,B}
|
||||||
|
↑
|
||||||
|
B→D=2+3=5 (relaksacja)
|
||||||
|
|
||||||
|
Krok 3: przeszukaj tablicę → min = C (4)
|
||||||
|
d = [ A:0, B:2, C:4, D:5 ] odw = {A,B,C}
|
||||||
|
↑
|
||||||
|
C→D=4+5=9 > 5, nie zmieniaj
|
||||||
|
|
||||||
|
Krok 4: min = D (5). Koniec! d = [A:0, B:2, C:4, D:5]
|
||||||
|
|
||||||
|
Każdy krok = przejrzyj V elementów → 4 kroki × 4 elementy = 16 operacji = O(V²)
|
||||||
|
|
||||||
|
**Kopiec (heap)** — drzewiasta struktura danych, w której element minimalny jest zawsze na szczycie. Wyciąganie minimum: O(log n). W Dijkstrze z kopcem: szukanie min d[v] to O(log V) zamiast O(V) → **O((V+E) log V)**.
|
||||||
|
|
||||||
|
Przykład — ten sam graf, ale z kopcem (min-heap):
|
||||||
|
|
||||||
|
Kopiec na starcie: (0,A) ← min zawsze na szczycie
|
||||||
|
(reszta to ∞)
|
||||||
|
|
||||||
|
Krok 1: pop (0,A) — O(log 4)=O(2), relaksuj sąsiadów:
|
||||||
|
push (2,B), push (4,C)
|
||||||
|
|
||||||
|
Kopiec: (2,B)
|
||||||
|
/ \
|
||||||
|
(4,C) ...
|
||||||
|
|
||||||
|
Krok 2: pop (2,B) — O(log 4), relaksuj:
|
||||||
|
push (5,D)
|
||||||
|
|
||||||
|
Kopiec: (4,C)
|
||||||
|
/
|
||||||
|
(5,D)
|
||||||
|
|
||||||
|
Krok 3: pop (4,C) — O(log 4). C→D: 9 > 5, nie zmieniaj.
|
||||||
|
Krok 4: pop (5,D) — O(log 4). Koniec!
|
||||||
|
|
||||||
|
Każdy pop = O(log V), każdy push = O(log V)
|
||||||
|
V popów + E pushów = O((V+E) log V)
|
||||||
|
|
||||||
|
**Kopiec Fibonacciego** — zaawansowany kopiec, w którym operacja „zmniejsz klucz" (decrease-key) działa w zamortyzowanym O(1) zamiast O(log V). Dijkstra robi decrease-key dla każdej krawędzi → z kopcem Fib: **O(V log V + E)** — E operacji po O(1) + V wyciągnięć po O(log V).
|
||||||
|
|
||||||
|
Przykład — kluczowa różnica: decrease-key:
|
||||||
|
|
||||||
|
Zwykły kopiec — gdy znajdziesz krótszą drogę do D:
|
||||||
|
d[D] zmienia się z 9 na 5
|
||||||
|
Trzeba „naprawić" kopiec: przesuwaj D w górę → O(log V)
|
||||||
|
|
||||||
|
Kopiec Fibonacciego — ta sama sytuacja:
|
||||||
|
d[D] zmienia się z 9 na 5
|
||||||
|
Po prostu odetnij D od rodzica i wstaw do listy korzeni → O(1)!
|
||||||
|
(naprawienie struktury odłożone na później — „zamortyzowane")
|
||||||
|
|
||||||
|
Różnica ma znaczenie przy GĘSTYCH grafach (E >> V):
|
||||||
|
- Zwykły kopiec: E × O(log V) = O(E log V) na decrease-key
|
||||||
|
- Kopiec Fib: E × O(1) = O(E) na decrease-key
|
||||||
|
Razem: O(V log V) [pop] + O(E) [decrease-key] = O(V log V + E)
|
||||||
|
|
||||||
|
**Złożoność — dlaczego takie wartości:**
|
||||||
|
|
||||||
|
- **O(V²)** z tablicą: V razy szukaj minimum (O(V) każdy) = V × V.
|
||||||
|
- **O((V+E) log V)** z kopcem: V wyciągnięć min (O(log V)) + E relaksacji z decrease-key (O(log V)).
|
||||||
|
- **O(V log V + E)** z kopcem Fib: V wyciągnięć min (O(log V)) + E decrease-key (O(1) zamortyzowane).
|
||||||
|
|
||||||
|
**Programowanie dynamiczne (DP)** — technika rozwiązywania problemów przez rozbicie na mniejsze podproblemy i zapamiętywanie wyników (żeby nie liczyć tego samego dwa razy). Bellman-Ford jest DP: podproblem = „najkrótsza ścieżka do v używająca ≤ k krawędzi"; rozwiązuje dla k = 1, 2, ..., V−1.
|
||||||
|
|
||||||
|
**Cykl** — ścieżka w grafie, która wraca do punktu wyjścia (A → B → C → A). **Cykl ujemny** — cykl, w którym suma wag < 0. Problem: za każdym obejściem cyklu „odległość" maleje — można iść w nieskończoność → najkrótsza ścieżka nie istnieje (= −∞).
|
||||||
|
|
||||||
|
**Dlaczego O(V·E) w Bellman-Ford:** Algorytm wykonuje |V|−1 iteracji (bo najdłuższa najkrótsza ścieżka bez cykli ma co najwyżej V−1 krawędzi). W każdej iteracji relaksuje WSZYSTKIE |E| krawędzi. Razem: (V−1) × E ≈ O(V·E).
|
||||||
|
|
||||||
|
**Heurystyczny** — wykorzystujący przybliżone oszacowanie (heurystykę) zamiast dokładnych obliczeń. A\* jest heurystyczny: używa funkcji h(n) do zgadywania „ile jeszcze do celu".
|
||||||
|
|
||||||
|
**f(n), g(n), h(n) — co oznacza n i każda funkcja:**
|
||||||
|
|
||||||
|
- **n** = aktualnie rozpatrywany wierzchołek.
|
||||||
|
- **g(n)** = dotychczasowy koszt dotarcia od startu do n (znany, dokładny).
|
||||||
|
- **h(n)** = heurystyka: OSZACOWANIE kosztu od n do celu (przybliżone, „zgadywane"). Np. odległość w linii prostej do celu.
|
||||||
|
- **f(n) = g(n) + h(n)** = oszacowanie CAŁKOWITEGO kosztu ścieżki przez n. A\* zawsze rozwija wierzchołek o najniższym f(n).
|
||||||
|
|
||||||
|
**Dopuszczalna (admissible)** — heurystyka h jest dopuszczalna, jeśli NIGDY nie przeszacowuje: h(n) ≤ prawdziwy koszt od n do celu. Gwarantuje, że A\* znajdzie optymalną ścieżkę. Np. odległość w linii prostej jest dopuszczalna (nie da się dojechać krócej niż prosto).
|
||||||
|
|
||||||
|
**Rzeczywisty koszt** — prawdziwa najkrótsza odległość (nie oszacowanie). Np. faktyczna najkrótsza droga od n do celu, uwzględniając wszystkie krawędzie.
|
||||||
|
|
||||||
|
**n → cel** — od wierzchołka n do wierzchołka docelowego (cel = destination = target).
|
||||||
|
|
||||||
|
**Spójna (consistent / monotone)** — silniejszy warunek na heurystykę: h(n) ≤ w(n,m) + h(m) dla każdej krawędzi n→m. Tu **w(n,m)** = waga krawędzi z n do m, a **m** = sąsiad n. Spójność oznacza: oszacowanie z n nie jest „dużo lepsze" niż to co uzyskasz idąc jeden krok do m. Spójna ⇒ dopuszczalna (ale nie odwrotnie).
|
||||||
|
|
||||||
|
**Dlaczego O(V) w najlepszym przypadku A\*:** Jeśli heurystyka jest idealna (h(n) = prawdziwy koszt), A* idzie prosto do celu, nie eksplorując zbędnych wierzchołków — odwiedza tylko te na optymalnej ścieżce ≈ O(V) jeśli ścieżka krótka. **Najgorszy przypadek** = h(n) = 0 dla wszystkich n → A* degeneruje się do Dijkstry.
|
||||||
|
|
||||||
|
### Pseudokod (Python)
|
||||||
|
|
||||||
|
**Dijkstra** (graph = słownik sąsiedztwa, np. `{'A': [('B',2), ('C',4)]}`):
|
||||||
|
|
||||||
|
def dijkstra(graph, source):
|
||||||
|
dist = {v: float('inf') for v in graph}
|
||||||
|
dist[source] = 0
|
||||||
|
visited = set()
|
||||||
|
for _ in range(len(graph)):
|
||||||
|
current = None # szukaj nieodwiedzonego wierzchołka o min dist — O(V)
|
||||||
|
for v in graph:
|
||||||
|
if v not in visited and (current is None or dist[v] < dist[current]):
|
||||||
|
current = v
|
||||||
|
if dist[current] == float('inf'):
|
||||||
|
break # reszta nieosiągalna
|
||||||
|
visited.add(current) # zamknij — NIE wracamy (zachłanność)
|
||||||
|
for neighbor, weight in graph[current]: # relaksacja sąsiadów
|
||||||
|
if dist[current] + weight < dist[neighbor]:
|
||||||
|
dist[neighbor] = dist[current] + weight
|
||||||
|
return dist # O(V²) z tablicą
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Bellman-Ford** (vertices = lista wierzchołków, edges = lista krotek (src, dst, weight)):
|
||||||
|
|
||||||
|
def bellman_ford(vertices, edges, source):
|
||||||
|
dist = {v: float('inf') for v in vertices}
|
||||||
|
dist[source] = 0
|
||||||
|
for _ in range(len(vertices) - 1): # V−1 iteracji (najdłuższa ścieżka = V−1 krawędzi)
|
||||||
|
for src, dst, weight in edges: # relaksuj WSZYSTKIE krawędzie
|
||||||
|
if dist[src] + weight < dist[dst]:
|
||||||
|
dist[dst] = dist[src] + weight
|
||||||
|
for src, dst, weight in edges: # V-ta iteracja: wykrywanie cyklu ujemnego
|
||||||
|
if dist[src] + weight < dist[dst]:
|
||||||
|
return None # cykl ujemny!
|
||||||
|
return dist # O(V·E)
|
||||||
|
|
||||||
|
Przykład — graf z ujemnymi wagami (Dijkstra daje ZŁY wynik, B-F poprawny):
|
||||||
|
|
||||||
|
Graf: S→A(2), A→C(3), S→B(5), B→A(−4)
|
||||||
|
|
||||||
|
Dijkstra:
|
||||||
|
1. S(0): dist[A]=2, dist[B]=5
|
||||||
|
2. A(2) zamknięty: dist[C]=5
|
||||||
|
3. B(5): B→A = 5−4 = 1 < 2, ALE A już zamknięty → POMIJA!
|
||||||
|
Wynik: A=2, C=5 ← BŁĄD (prawidłowe: A=1, C=4)
|
||||||
|
|
||||||
|
Bellman-Ford — relaksuje WSZYSTKIE krawędzie, V−1 = 3 razy:
|
||||||
|
Start: dist = [S:0, A:∞, B:∞, C:∞]
|
||||||
|
|
||||||
|
Iteracja 1:
|
||||||
|
S→A: 0+2=2 < ∞ → A=2
|
||||||
|
A→C: 2+3=5 < ∞ → C=5
|
||||||
|
S→B: 0+5=5 < ∞ → B=5
|
||||||
|
B→A: 5−4=1 < 2 → A=1 ← ujemna waga poprawia!
|
||||||
|
|
||||||
|
Iteracja 2:
|
||||||
|
A→C: 1+3=4 < 5 → C=4 ← propagacja poprawionego A
|
||||||
|
|
||||||
|
Iteracja 3: brak zmian → stabilne.
|
||||||
|
Wynik: [S:0, A:1, B:5, C:4] ← POPRAWNE
|
||||||
|
|
||||||
|
Wykrywanie cyklu ujemnego — dodaj krawędź C→B(−3):
|
||||||
|
Cykl B→A→C→B = −4 + 3 + (−3) = −4 < 0.
|
||||||
|
Po V−1 iteracjach dist nadal maleje → V-ta iteracja:
|
||||||
|
dist[src] + weight < dist[dst] → return None
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**A\*** (graph jak Dijkstra; heuristic = h(v) → oszacowanie odl. do celu):
|
||||||
|
|
||||||
|
def a_star(graph, source, goal, heuristic):
|
||||||
|
cost_so_far = {source: 0} # g(n) — faktyczny koszt dotarcia
|
||||||
|
priority = {source: heuristic(source)} # f(n) = g(n) + h(n)
|
||||||
|
came_from = {} # do odtworzenia ścieżki
|
||||||
|
visited = set()
|
||||||
|
while priority:
|
||||||
|
current = min(priority, key=priority.get) # wierzchołek o min f(n)
|
||||||
|
del priority[current]
|
||||||
|
if current == goal:
|
||||||
|
break # dotarliśmy — A* kończy (Dijkstra przetworzyłby wszystko)
|
||||||
|
visited.add(current)
|
||||||
|
for neighbor, weight in graph[current]:
|
||||||
|
if neighbor in visited:
|
||||||
|
continue
|
||||||
|
new_cost = cost_so_far[current] + weight
|
||||||
|
if neighbor not in cost_so_far or new_cost < cost_so_far[neighbor]:
|
||||||
|
cost_so_far[neighbor] = new_cost
|
||||||
|
priority[neighbor] = new_cost + heuristic(neighbor)
|
||||||
|
came_from[neighbor] = current
|
||||||
|
return came_from, cost_so_far.get(goal) # ścieżka + koszt
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Dijkstra — zachłanny, SSSP
|
||||||
|
|
||||||
|
**Ograniczenie:** wagi ≥ 0.
|
||||||
|
**Idea:** Relaksacja krawędzi; zawsze przetwarzaj wierzchołek o najmniejszym d[v].
|
||||||
|
**Złożoność:** O(V²) z tablicą, O((V+E) log V) z kopcem, O(V log V + E) z kopcem Fibonacciego.
|
||||||
|
**Dlaczego nie ujemne wagi?** Raz oznaczony wierzchołek nie jest rewidowany — ujemna krawędź może go poprawić.
|
||||||
|
|
||||||
|
### Bellman-Ford — programowanie dynamiczne, SSSP
|
||||||
|
|
||||||
|
**Zaleta:** obsługuje ujemne wagi + **wykrywa cykle ujemne**.
|
||||||
|
**Idea:** |V|−1 iteracji relaksacji WSZYSTKICH krawędzi. Jeśli w iteracji V nadal można poprawić → cykl ujemny.
|
||||||
|
**Złożoność:** O(V·E) — zawsze.
|
||||||
|
|
||||||
|
### A\* — heurystyczny, Single-Pair
|
||||||
|
|
||||||
|
**Rozszerzenie Dijkstry:** f(n) = g(n) + h(n), gdzie h(n) to heurystyka.
|
||||||
|
**Wymóg:** h dopuszczalna (admissible): h(n) ≤ rzeczywisty koszt n→cel. Jeśli h spójna (consistent): h(n) ≤ w(n,m) + h(m) — optymalne.
|
||||||
|
**Złożoność:** zależy od h; najlepszy przypadek O(V), najgorszy jak Dijkstra.
|
||||||
|
|
||||||
|
### Porównanie
|
||||||
|
|
||||||
|
| Cecha | Dijkstra | Bellman-Ford | A\* |
|
||||||
|
| -------------- | ------------- | ---------------- | ------------ |
|
||||||
|
| Typ | Zachłanny | Prog. dynamiczne | Heurystyczny |
|
||||||
|
| Problem | SSSP | SSSP | Single-pair |
|
||||||
|
| Ujemne wagi | NIE | TAK | NIE |
|
||||||
|
| Wykrywa cykle- | NIE | TAK | NIE |
|
||||||
|
| Złożoność | O((V+E)log V) | O(VE) | Zależy od h |
|
||||||
|
|
||||||
|
### Etymologia
|
||||||
|
|
||||||
|
**Dijkstra** — Edsger W. Dijkstra (Holandia, 1959); pionier informatyki (Turing Award 1972). **Bellman-Ford** — Richard Bellman (twórca programowania dynamicznego) + Lester Ford Jr. (1956). **A\*** — Hart, Nilsson, Raphael (Stanford, 1968); „A\*" = ulepszona wersja algorytmu „A". **Zachłanny (Greedy)** — algorytm „chciwie" bierze lokalnie najlepszą opcję. **SSSP** — Single-Source Shortest Path. **Programowanie dynamiczne** — Bellman wybrał „dynamic" by brzmiało imponująco dla polityków (nie miało związku z dynamiką!). **Heurystyka** — grec. „heuriskein" = znajdować (to samo co „Eureka!" Archimedesa). **Relaksacja** — „rozluźnianie" górnego ograniczenia na odległość d[v].
|
||||||
|
|
||||||
|
### Jak zapamiętać
|
||||||
|
|
||||||
|
- **Dijkstra = chciwy**, bierze minimum — ale „nie patrzy wstecz" (stąd problem z ujemnymi wagami)
|
||||||
|
- **Bellman-Ford = brute force x (V−1)** — relaksuj wszystko, V−1 razy, bo najdłuższa ścieżka ma V−1 krawędzi
|
||||||
|
- **A\* = Dijkstra + „GPS"** — heurystyka mówi w którą stronę jest cel
|
||||||
127
python_pkg/praca_magisterska_video/generate_images/anki_approach_1.py
Executable file
@ -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"<b>\1</b>", text)
|
||||||
|
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<i>\1</i>", 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 = (
|
||||||
|
"<ul>"
|
||||||
|
+ "".join([f"<li>{clean_text(h)}</li>" for h in headers[:6]])
|
||||||
|
+ "</ul>"
|
||||||
|
)
|
||||||
|
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()
|
||||||
152
python_pkg/praca_magisterska_video/generate_images/anki_approach_2.py
Executable file
@ -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"<b>\1</b>", text)
|
||||||
|
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<i>\1</i>", 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"<b>Definicja:</b> {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"• <b>{term}</b>: {desc.strip()}")
|
||||||
|
else:
|
||||||
|
parts.append(f"• <b>{term}</b>")
|
||||||
|
|
||||||
|
# 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"<b>{k}</b>: {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 "<br>".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()
|
||||||
426
python_pkg/praca_magisterska_video/generate_images/anki_generator.py
Executable file
@ -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"<b>\1</b>", text)
|
||||||
|
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<i>\1</i>", 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"<b>Definicja:</b> {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"• <b>{term}</b>: {desc.strip()}")
|
||||||
|
else:
|
||||||
|
parts.append(f"• <b>{term}</b>")
|
||||||
|
|
||||||
|
# 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"<b>{k.strip()}</b>: {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 "<br>".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## [^<5E>]|\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 = (
|
||||||
|
"<ul>"
|
||||||
|
+ "".join([f"<li>{clean_text(h)}</li>" for h in headers[:6]])
|
||||||
|
+ "</ul>"
|
||||||
|
)
|
||||||
|
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## [^<5E>]|\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"<b>{header}</b>: {key_point}")
|
||||||
|
|
||||||
|
if answer_parts:
|
||||||
|
answer = "<br><br>".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()
|
||||||
767
python_pkg/praca_magisterska_video/generate_images/generate_agent_diagrams.py
Executable file
@ -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)
|
||||||
224
python_pkg/praca_magisterska_video/generate_images/generate_anki.py
Executable file
@ -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 = "<br>".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 = "<br>".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"<b>\1</b>", text)
|
||||||
|
text = re.sub(r"\*(.+?)\*", r"<i>\1</i>", text)
|
||||||
|
# Handle newlines - convert to <br> for Anki
|
||||||
|
text = text.replace("\n", "<br>")
|
||||||
|
# Remove multiple <br>
|
||||||
|
text = re.sub(r"(<br>)+", "<br>", text)
|
||||||
|
# Remove leading/trailing <br>
|
||||||
|
text = re.sub(r"^<br>|<br>$", "", 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()
|
||||||
329
python_pkg/praca_magisterska_video/generate_images/generate_anki_final.py
Executable file
@ -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"<b>\1</b>", text)
|
||||||
|
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<i>\1</i>", 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"<li>{cleaned}</li>"
|
||||||
|
html += f"</{tag}>"
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
def extract_from_file(filepath) -> list[dict[str, str]]:
|
||||||
|
"""Extract flashcard data from a markdown file."""
|
||||||
|
with Path(filepath).open(encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
cards = []
|
||||||
|
|
||||||
|
# Get file metadata
|
||||||
|
filename = Path(filepath).name
|
||||||
|
match = re.match(r"(\d+)-(.+)\.md", filename)
|
||||||
|
num = match.group(1) if match else "00"
|
||||||
|
match.group(2).replace("-", "_") if match else "unknown"
|
||||||
|
|
||||||
|
# Extract subject
|
||||||
|
subj_match = re.search(r"Przedmiot:\s*(\w+)", content)
|
||||||
|
subject = subj_match.group(1) if subj_match else "Ogólne"
|
||||||
|
|
||||||
|
# Base tags
|
||||||
|
base_tags = f"egzamin_magisterski pyt{num} {subject}"
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# CARD TYPE 1: Main Exam Question
|
||||||
|
# =====================================================
|
||||||
|
q_match = re.search(
|
||||||
|
r'## Pytanie\s*\n\s*\*\*["\']?(.+?)["\']?\*\*', content, re.DOTALL
|
||||||
|
)
|
||||||
|
if q_match:
|
||||||
|
main_q = re.sub(r"\s+", " ", q_match.group(1).strip())
|
||||||
|
|
||||||
|
# Extract key topics from main answer
|
||||||
|
answer_match = re.search(
|
||||||
|
r"## 📚 Odpowiedź główna\s*\n(.+?)(?=\n## [<5B>🎯]|\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 = "<b>Kluczowe zagadnienia:</b>" + 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"<b>{term}</b>: {desc.strip()}")
|
||||||
|
else:
|
||||||
|
answer_parts.append(f"<b>{term}</b>")
|
||||||
|
|
||||||
|
# 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 = "<table><tr><th>Aspekt</th><th>Wartość</th></tr>"
|
||||||
|
for aspect, value in items[:6]:
|
||||||
|
comparison_html += f"<tr><td>{clean_text(aspect)}</td><td>{clean_text(value)}</td></tr>"
|
||||||
|
comparison_html += "</table>"
|
||||||
|
|
||||||
|
# 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", "<br>"),
|
||||||
|
"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()
|
||||||
202
python_pkg/praca_magisterska_video/generate_images/generate_anki_v2.py
Executable file
@ -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## [^<5E>]|\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"<b>\1</b>", text)
|
||||||
|
text = re.sub(r"\*(.+?)\*", r"<i>\1</i>", text)
|
||||||
|
|
||||||
|
# Clean up special characters
|
||||||
|
text = text.replace("\t", " ")
|
||||||
|
text = text.replace('"', """)
|
||||||
|
|
||||||
|
# Handle newlines - convert to <br>
|
||||||
|
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 = (
|
||||||
|
"<ul>"
|
||||||
|
+ "".join([f"<li>{clean_html(p)}</li>" for p in key_points])
|
||||||
|
+ "</ul>"
|
||||||
|
)
|
||||||
|
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()
|
||||||
307
python_pkg/praca_magisterska_video/generate_images/generate_anki_v3.py
Executable file
@ -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"<b>\1</b>", text)
|
||||||
|
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<i>\1</i>", 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"<b>{sub_header.strip()}</b>: {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"• <b>{term.strip()}</b>: {desc.strip()}")
|
||||||
|
else:
|
||||||
|
lines.append(f"• <b>{term.strip()}</b>")
|
||||||
|
|
||||||
|
# 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 "<br>".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"<b>Automat Skończony (FA)</b>: {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"<b>Automat ze Stosem (PDA)</b>: {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"<b>Maszyna Turinga (TM)</b>: {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 = "<br><br>".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"• <b>{term}</b>: {desc.strip()}" if desc else f"• <b>{term}</b>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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"• <b>{term}</b>: {desc.strip()}" if desc else f"• <b>{term}</b>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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 = "<br>".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": "<br>".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()
|
||||||
1036
python_pkg/praca_magisterska_video/generate_images/generate_arch_diagrams.py
Executable file
887
python_pkg/praca_magisterska_video/generate_images/generate_automata_diagrams.py
Executable file
@ -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}/")
|
||||||
@ -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}/")
|
||||||
952
python_pkg/praca_magisterska_video/generate_images/generate_pattern_diagrams.py
Executable file
@ -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!")
|
||||||
708
python_pkg/praca_magisterska_video/generate_images/generate_process_diagrams.py
Executable file
@ -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)")
|
||||||
747
python_pkg/praca_magisterska_video/generate_images/generate_pubsub_diagrams.py
Executable file
@ -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!")
|
||||||
2114
python_pkg/praca_magisterska_video/generate_images/generate_q20_diagrams.py
Executable file
2722
python_pkg/praca_magisterska_video/generate_images/generate_q23_diagrams.py
Executable file
2270
python_pkg/praca_magisterska_video/generate_images/generate_q24_diagrams.py
Executable file
1131
python_pkg/praca_magisterska_video/generate_images/generate_q31_diagrams.py
Executable file
1598
python_pkg/praca_magisterska_video/generate_images/generate_q9_all_diagrams.py
Executable file
1132
python_pkg/praca_magisterska_video/generate_images/generate_q9_q12_diagrams.py
Executable file
@ -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)
|
||||||
1418
python_pkg/praca_magisterska_video/generate_images/generate_scheduling_diagrams.py
Executable file
@ -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}/")
|
||||||
1064
python_pkg/praca_magisterska_video/generate_images/generate_study_diagrams.py
Executable file
276
python_pkg/praca_magisterska_video/generate_images/print_questions.sh
Executable file
@ -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
|
||||||
59
python_pkg/praca_magisterska_video/generate_images/split_questions.py
Executable file
@ -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}")
|
||||||
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 386 KiB |
|
After Width: | Height: | Size: 583 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 164 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/c4_model.png
Normal file
|
After Width: | Height: | Size: 335 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/cpm_example.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 253 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/epc_reklamacja.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 177 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/ipc_mechanisms.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 324 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 240 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/nf_0nf_table.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/nf_1nf_tables.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/nf_2nf_tables.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/nf_3nf_tables.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/nf_4nf_example.png
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/nf_5nf_example.png
Normal file
|
After Width: | Height: | Size: 418 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/nf_bcnf_tables.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 178 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/paxos_flow.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 385 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 128 KiB |
BIN
python_pkg/praca_magisterska_video/images/img/q20_flink_arch.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 289 KiB |
|
After Width: | Height: | Size: 253 KiB |