From 6e1648f264ce7d76095b0443de682dd2c83aabe9 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 14 Mar 2026 14:29:54 +0100 Subject: [PATCH] refactor(praca/generate_images): fix ruff violations in diagram generators batch 1 --- .../generate_bf_negative_diagram.py | 380 ++++++---- .../generate_normalization_diagrams.py | 48 +- .../generate_pattern_diagrams.py | 285 +++++--- .../generate_images/generate_q31_diagrams.py | 677 ++++++++++-------- .../generate_robot_lang_diagrams.py | 185 +++-- .../generate_shortest_path_diagrams.py | 287 +++++--- .../generate_images/split_questions.py | 18 +- 7 files changed, 1201 insertions(+), 679 deletions(-) diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_bf_negative_diagram.py b/python_pkg/praca_magisterska_video/generate_images/generate_bf_negative_diagram.py index 34a70e6..fd2faa4 100755 --- a/python_pkg/praca_magisterska_video/generate_images/generate_bf_negative_diagram.py +++ b/python_pkg/praca_magisterska_video/generate_images/generate_bf_negative_diagram.py @@ -1,13 +1,18 @@ #!/usr/bin/env python3 -"""Generate Bellman-Ford negative-weights & negative-cycle diagram for PYTANIE 2. +"""Generate Bellman-Ford negative-weights & negative-cycle diagram. -Two-part figure: +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)) + Part 2: Negative cycle detection (add C->B(-3)) A4-compatible, monochrome-friendly, 300 DPI. """ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + import matplotlib as mpl mpl.use("Agg") @@ -16,6 +21,11 @@ from pathlib import Path import matplotlib.pyplot as plt import numpy as np +if TYPE_CHECKING: + from matplotlib.axes import Axes + +_logger = logging.getLogger(__name__) + DPI = 300 BG = "white" LN = "black" @@ -34,34 +44,53 @@ 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)] +# Graph layout for negative-weight example: +# S->A(2), A->C(3), S->B(5), B->A(-4) +NEG_POS: dict[str, tuple[float, float]] = { + "S": (0.8, 2), + "A": (3.3, 3.2), + "B": (3.3, 0.8), + "C": (5.8, 2), +} +NEG_EDGES: list[tuple[str, str, int]] = [ + ("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, + ax: Axes, + name: str, + pos: tuple[float, float], + *, + color: str = "white", + current: bool = False, + visited: bool = False, + dist_label: str | None = None, + fontsize: int = 12, + error: bool = False, ) -> None: - """Draw node.""" + """Draw a graph node with optional distance label.""" 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) + 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 + (x, y), + r, + fill=True, + facecolor=fc, + edgecolor=ec, + linewidth=lw, + zorder=5, ) ax.add_patch(circle) ax.text( @@ -95,18 +124,36 @@ def draw_node( ) +def _choose_edge_style( + *, + negative: bool, + relaxed: bool, + highlighted: bool, + cycle_edge: bool, +) -> tuple[str, float, str]: + """Return (color, lw, linestyle) for an edge.""" + if cycle_edge: + return "#D32F2F", 2.5, "--" + if negative or relaxed: + return "#D32F2F", 2.5, "-" + if highlighted: + return "#1565C0", 2.0, "-" + return GRAY3, 1.5, "-" + + def draw_edge( - ax, - pos1, - pos2, - weight, - highlighted=False, - relaxed=False, - negative=False, - cycle_edge=False, - offset=0.0, + ax: Axes, + pos1: tuple[float, float], + pos2: tuple[float, float], + weight: int, + *, + highlighted: bool = False, + relaxed: bool = False, + negative: bool = False, + cycle_edge: bool = False, + offset: float = 0.0, ) -> None: - """Draw edge.""" + """Draw a directed edge between two nodes with a weight label.""" x1, y1 = pos1 x2, y2 = pos2 @@ -127,22 +174,12 @@ def draw_edge( 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 = "-" + color, lw, ls = _choose_edge_style( + negative=negative, + relaxed=relaxed, + highlighted=highlighted, + cycle_edge=cycle_edge, + ) # Arrow ax.annotate( @@ -191,18 +228,19 @@ def draw_edge( 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, + ax: Axes, + edges: list[tuple[str, str, int]], + *, + title: str = "", + dist: dict[str, str] | None = None, + current: str | None = None, + visited: set[str] | None = None, + relaxed_edges: set[tuple[str, str]] | None = None, + error_nodes: set[str] | None = None, + extra_edges: list[tuple[str, str, int]] | None = None, + node_positions: dict[str, tuple[float, float]] | None = None, ) -> None: - """Draw neg graph.""" + """Draw the negative-weight graph with annotations.""" if visited is None: visited = set() if relaxed_edges is None: @@ -228,9 +266,7 @@ def draw_neg_graph( 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 + cycle = bool(extra_edges and (u, v, w) in extra_edges) draw_edge( ax, node_positions[u], @@ -239,7 +275,7 @@ def draw_neg_graph( relaxed=rl, negative=neg, cycle_edge=cycle, - offset=off, + offset=0.0, ) for name, pos in node_positions.items(): @@ -258,32 +294,62 @@ def draw_neg_graph( ) +def _add_annotation_box( + ax: Axes, + x: float, + y: float, + text: str, + *, + color: str, + bg_color: str, +) -> None: + """Add a small annotation box near a node.""" + ax.text( + x, + y, + text, + fontsize=FS_SMALL, + color=color, + fontweight="bold", + bbox={ + "boxstyle": "round,pad=0.1", + "facecolor": bg_color, + "edgecolor": color, + "alpha": 0.9, + "lw": 0.5, + }, + ) + + def generate_bf_negative_weights() -> None: - """Two-row figure. + """Generate 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", + "Bellman-Ford \u2014 ujemne wagi vs Dijkstra\n" + "Graf: S\u2192A(2), A\u2192C(3)," + " S\u2192B(5), B\u2192A(-4). Start = S", fontsize=FS_TITLE + 1, fontweight="bold", y=0.99, ) - # ---- Row 1: Graph + Dijkstra wrong + BF correct ---- + # 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)", + 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]), @@ -291,7 +357,11 @@ def generate_bf_negative_weights() -> None: fontsize=FS, fontweight="bold", color="#D32F2F", - arrowprops={"arrowstyle": "->", "color": "#D32F2F", "lw": 2}, + arrowprops={ + "arrowstyle": "->", + "color": "#D32F2F", + "lw": 2, + }, va="center", ) @@ -300,41 +370,29 @@ def generate_bf_negative_weights() -> None: draw_neg_graph( ax2, NEG_EDGES, - title="Dijkstra — BŁĘDNY wynik\nA zamknięty z d=2, nie poprawia przy B→A", + title=( + "Dijkstra \u2014 BŁĘDNY wynik\n" + "A 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( + _add_annotation_box( + ax2, 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, - }, + bg_color=LIGHT_RED, ) - ax2.text( + _add_annotation_box( + ax2, 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, - }, + bg_color=LIGHT_RED, ) # Panel 3: Bellman-Ford — CORRECT @@ -342,48 +400,45 @@ def generate_bf_negative_weights() -> None: draw_neg_graph( ax3, NEG_EDGES, - title="Bellman-Ford — POPRAWNY wynik\nUjemna waga B→A poprawnie propagowana", + title=( + "Bellman-Ford \u2014 POPRAWNY wynik\n" + "Ujemna 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( + _add_annotation_box( + ax3, 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, - }, + bg_color=LIGHT_GREEN, ) - ax3.text( + _add_annotation_box( + ax3, 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, - }, + bg_color=LIGHT_GREEN, ) - # ---- Row 2: B-F iterations step by step ---- + # 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")}, + "title": ( + "B-F Iteracja 1\n" + "Relaksuj 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" @@ -392,16 +447,29 @@ def generate_bf_negative_weights() -> None: ), }, { - "title": "B-F Iteracja 2\nPropagacja poprawionego A", - "dist": {"S": "0", "A": "1", "B": "5", "C": "4"}, + "title": ( + "B-F Iteracja 2\n" + "Propagacja 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 ✗" + "S→A: 0+2=2>1 ✗\n" + "A→C: 1+3=4<5 → C=4 ✓\n" + "S→B: 0+5=5=5 ✗\n" + "B→A: 5-4=1=1 ✗" ), }, { - "title": "B-F Iteracja 3\nBrak zmian → stabilne!", - "dist": {"S": "0", "A": "1", "B": "5", "C": "4"}, + "title": ( + "B-F Iteracja 3\n" + "Brak zmian → stabilne!" + ), + "dist": { + "S": "0", "A": "1", "B": "5", "C": "4", + }, "relaxed": set(), "detail": ( "Wszystkie krawędzie:\n" @@ -422,7 +490,6 @@ def generate_bf_negative_weights() -> None: visited={"S", "A", "B", "C"}, relaxed_edges=it["relaxed"], ) - # Detail text below graph ax.text( 3.2, -0.5, @@ -431,19 +498,31 @@ def generate_bf_negative_weights() -> None: va="top", fontsize=FS_SMALL, family="monospace", - bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3}, + 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.", + "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}, + bbox={ + "boxstyle": "round,pad=0.3", + "facecolor": LIGHT_YELLOW, + "edgecolor": LN, + }, ) plt.tight_layout(rect=[0, 0.05, 1, 0.95]) @@ -454,19 +533,20 @@ def generate_bf_negative_weights() -> None: facecolor=BG, ) plt.close() - print(" ✓ bellman_ford_negative_weights.png") + _logger.info(" ✓ bellman_ford_negative_weights.png") def generate_bf_negative_cycle() -> None: - """Figure showing negative cycle detection. + """Generate 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. + Graph: S->A(2), A->C(3), S->B(5), B->A(-4), C->B(-3) + 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", + "Bellman-Ford \u2014 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, @@ -477,11 +557,13 @@ def generate_bf_negative_cycle() -> None: draw_neg_graph( ax1, NEG_EDGES, - title="Graf z cyklem ujemnym\nDodana krawędź C→B(-3) — przerywana", + title=( + "Graf z cyklem ujemnym\n" + "Dodana krawędź C→B(-3) \u2014 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), @@ -503,7 +585,10 @@ def generate_bf_negative_cycle() -> None: draw_neg_graph( ax2, NEG_EDGES, - title="Po V-1=3 iteracjach\ndist wciąż maleje (niestabilne!)", + title=( + "Po V-1=3 iteracjach\n" + "dist wciąż maleje (niestabilne!)" + ), dist={"S": "0", "A": "-7", "B": "-4", "C": "-4"}, visited={"S", "A", "B", "C"}, error_nodes={"A", "B", "C"}, @@ -512,7 +597,9 @@ def generate_bf_negative_cycle() -> None: ax2.text( 3.2, -0.4, - "Każde okrążenie cyklu\nzmniejsza dist o 4.\nDist → -∞ (brak minimum!)", + "Każde okrążenie cyklu\n" + "zmniejsza dist o 4.\n" + "Dist → -∞ (brak minimum!)", ha="center", va="top", fontsize=FS_SMALL, @@ -562,7 +649,8 @@ def generate_bf_negative_cycle() -> None: }, ) ax3.set_title( - "Wykrywanie — V-ta iteracja\nJeśli cokolwiek się poprawia → cykl ujemny!", + "Wykrywanie \u2014 V-ta iteracja\n" + "Jeśli cokolwiek się poprawia → cykl ujemny!", fontsize=FS, fontweight="bold", pad=5, @@ -572,12 +660,19 @@ def generate_bf_negative_cycle() -> None: 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.", + "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}, + bbox={ + "boxstyle": "round,pad=0.3", + "facecolor": LIGHT_YELLOW, + "edgecolor": LN, + }, ) plt.tight_layout(rect=[0, 0.06, 1, 0.94]) @@ -588,11 +683,14 @@ def generate_bf_negative_cycle() -> None: facecolor=BG, ) plt.close() - print(" ✓ bellman_ford_negative_cycle.png") + _logger.info(" ✓ bellman_ford_negative_cycle.png") if __name__ == "__main__": - print("Generating Bellman-Ford negative weight diagrams...") + logging.basicConfig(level=logging.INFO) + _logger.info( + "Generating Bellman-Ford negative weight diagrams..." + ) generate_bf_negative_weights() generate_bf_negative_cycle() - print(f"\nAll diagrams saved to {OUTPUT_DIR}/") + _logger.info("All diagrams saved to %s/", OUTPUT_DIR) diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_normalization_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_normalization_diagrams.py index 27a554b..0ab8e5d 100755 --- a/python_pkg/praca_magisterska_video/generate_images/generate_normalization_diagrams.py +++ b/python_pkg/praca_magisterska_video/generate_images/generate_normalization_diagrams.py @@ -582,7 +582,7 @@ def draw_3nf() -> None: """Draw 3nf.""" fig, ax = create_figure(11.69, 6.5) - # Studenci (fixed) + # Student table after removing transitive dependency h1 = ["StID*", "Imie", "WydzialID"] r1 = [["1", "Anna", "W4"], ["2", "Jan", "W4"], ["3", "Ewa", "W2"]] cw1 = [0.55, 0.55, 0.85] @@ -633,7 +633,8 @@ def draw_3nf() -> None: ax, 0.3, 2.95, - " StID -> WydzialID -> NazwaWydzialu rozbito: NazwaWydzialu w osobnej tabeli.", + " StID -> WydzialID -> NazwaWydzialu" + " rozbito: NazwaWydzialu w osobnej tabeli.", fontsize=8, color="#333333", ) @@ -665,7 +666,8 @@ def draw_3nf() -> None: ax, 0.3, 1.5, - " BCNF nie ma takiego wyjatku -> kazda nietrywialna FD wymaga nadklucza po lewej.", + " BCNF nie ma takiego wyjatku" + " -> kazda nietrywialna FD wymaga nadklucza po lewej.", fontsize=9, color="#333333", ) @@ -677,7 +679,7 @@ def draw_3nf() -> None: pad_inches=0.2, ) plt.close(fig) - print("Generated: nf_3nf_tables.png") + logger.info("Generated: nf_3nf_tables.png") # ============================================================ @@ -713,7 +715,7 @@ def draw_bcnf() -> None: ax, 7.2, 6.8, "ProwadzacyKurs (kl: Prow.)", h4, r4, cw4, title_fontsize=9 ) - # StudentProwadzacy (NEW) + # New student-advisor junction table h5 = ["StID*", "Prowadzacy*"] r5 = [["1", "Kowalski"], ["1", "Nowak"], ["2", "Kowalski"], ["3", "Wisniewski"]] cw5 = [0.55, 1.05] @@ -733,7 +735,8 @@ def draw_bcnf() -> None: ax, 0.3, 2.55, - " ProwadzacyKurs(Prowadzacy, KursID) — FD: Prowadzacy -> KursID, klucz: Prowadzacy", + " ProwadzacyKurs(Prowadzacy, KursID)" + " — FD: Prowadzacy -> KursID, klucz: Prowadzacy", fontsize=8, color="#333333", ) @@ -756,7 +759,8 @@ def draw_bcnf() -> None: ax, 0.3, 1.45, - "Rekonstrukcja: StudentProw. JOIN ProwadzacyKurs ON Prowadzacy -> odtworzenie Zapisy.", + "Rekonstrukcja: StudentProw. JOIN ProwadzacyKurs" + " ON Prowadzacy -> odtworzenie Zapisy.", fontsize=8, color="#333333", ) @@ -768,7 +772,7 @@ def draw_bcnf() -> None: pad_inches=0.2, ) plt.close(fig) - print("Generated: nf_bcnf_tables.png") + logger.info("Generated: nf_bcnf_tables.png") # ============================================================ @@ -903,7 +907,7 @@ def draw_4nf() -> None: pad_inches=0.2, ) plt.close(fig) - print("Generated: nf_4nf_example.png") + logger.info("Generated: nf_4nf_example.png") # ============================================================ @@ -1013,14 +1017,16 @@ def draw_5nf() -> None: ax, 0.3, 2.0, - "Weryfikacja: Alfa dostarcza Nakretke? Alfa -> Wiezowiec? Nakretka -> Wiezowiec?", + "Weryfikacja: Alfa dostarcza Nakretke?" + " Alfa -> Wiezowiec? Nakretka -> Wiezowiec?", fontsize=8, ) add_label( ax, 0.3, 1.65, - " TAK, TAK, TAK --> wg reguly cyklicznej: Alfa dostarcza Nakretke do Wiezowca.", + " TAK, TAK, TAK --> wg reguly cyklicznej:" + " Alfa dostarcza Nakretke do Wiezowca.", fontsize=8, color="#333333", ) @@ -1035,7 +1041,8 @@ def draw_5nf() -> None: ax, 0.3, 0.9, - " --> Alfa dostarcza Nakretke do Mostu. (Tego wiersza NIE MA w oryginale -- BLAD!)", + " --> Alfa dostarcza Nakretke do Mostu." + " (Tego wiersza NIE MA w oryginale -- BLAD!)", fontsize=8, color="black", ) @@ -1043,7 +1050,8 @@ def draw_5nf() -> None: ax, 0.3, 0.5, - " Dekompozycja 5NF jest poprawna TYLKO jesli regula cykliczna rzeczywiscie zachodzi!", + " Dekompozycja 5NF jest poprawna TYLKO" + " jesli regula cykliczna rzeczywiscie zachodzi!", fontsize=8, color="black", ) @@ -1055,7 +1063,7 @@ def draw_5nf() -> None: pad_inches=0.2, ) plt.close(fig) - print("Generated: nf_5nf_example.png") + logger.info("Generated: nf_5nf_example.png") # ============================================================ @@ -1142,7 +1150,7 @@ def draw_summary_flow() -> None: arrowprops={"arrowstyle": "<-", "color": "black", "lw": 1.5}, ) - # Bottom: mnemonic + # Mnemonic quote at the bottom ax.text( 5.85, 2.2, @@ -1178,7 +1186,8 @@ def draw_summary_flow() -> None: ax.text( 5.85, 0.8, - "5NF (zawiera sie w) 4NF (zaw.) BCNF (zaw.) 3NF (zaw.) 2NF (zaw.) 1NF", + "5NF (zawiera sie w) 4NF (zaw.) BCNF" + " (zaw.) 3NF (zaw.) 2NF (zaw.) 1NF", fontsize=8, ha="center", va="center", @@ -1193,14 +1202,15 @@ def draw_summary_flow() -> None: pad_inches=0.2, ) plt.close(fig) - print("Generated: nf_summary_flow.png") + logger.info("Generated: nf_summary_flow.png") # ============================================================ # Main # ============================================================ if __name__ == "__main__": - print("Generating normalization diagrams...") + logging.basicConfig(level=logging.INFO) + logger.info("Generating normalization diagrams...") draw_0nf() draw_1nf() draw_2nf() @@ -1209,4 +1219,4 @@ if __name__ == "__main__": draw_4nf() draw_5nf() draw_summary_flow() - print("\nDone! All diagrams saved to:", OUTPUT_DIR) + logger.info("Done! All diagrams saved to %s", OUTPUT_DIR) diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_pattern_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_pattern_diagrams.py index d3cd02a..c7cf596 100755 --- a/python_pkg/praca_magisterska_video/generate_images/generate_pattern_diagrams.py +++ b/python_pkg/praca_magisterska_video/generate_images/generate_pattern_diagrams.py @@ -8,6 +8,11 @@ All: A4-compatible, B&W, 300 DPI, laser-printer-friendly. """ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + import matplotlib as mpl mpl.use("Agg") @@ -18,6 +23,11 @@ from matplotlib.patches import FancyBboxPatch import matplotlib.pyplot as plt import numpy as np +if TYPE_CHECKING: + from matplotlib.axes import Axes + +_logger = logging.getLogger(__name__) + DPI = 300 BG = "white" LN = "black" @@ -33,29 +43,40 @@ GRAY3 = "#B8B8B8" GRAY4 = "#F5F5F5" GRAY5 = "#C0C0C0" +_BAND_HEIGHTS = [0.7, 1.3, 1.4, 1.5, 1.5] + def draw_box( - ax, - x, - y, - w, - h, - text, - fill="white", - lw=1.2, - fontsize=FS, - fontweight="normal", - ha="center", - va="center", - rounded=True, + ax: Axes, + x: float, + y: float, + w: float, + h: float, + text: str, + *, + fill: str = "white", + lw: float = 1.2, + fontsize: float = FS, + fontweight: str = "normal", + ha: str = "center", + va: str = "center", + rounded: bool = True, ) -> None: - """Draw box.""" + """Draw a labeled box on the axes.""" if rounded: rect = FancyBboxPatch( - (x, y), w, h, boxstyle="round,pad=0.08", lw=lw, edgecolor=LN, facecolor=fill + (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) + rect = mpatches.Rectangle( + (x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill + ) ax.add_patch(rect) ax.text( x + w / 2, @@ -69,8 +90,18 @@ def draw_box( ) -def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None: - """Draw arrow.""" +def draw_arrow( + ax: Axes, + x1: float, + y1: float, + x2: float, + y2: float, + *, + lw: float = 1.2, + style: str = "->", + color: str = LN, +) -> None: + """Draw an arrow between two points.""" ax.annotate( "", xy=(x2, y2), @@ -83,7 +114,7 @@ def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None: # 1. Pattern Template Structure (NaPSiRoKo mnemonic) # ============================================================ def generate_pattern_template() -> None: - """Generate pattern template.""" + """Generate pattern template diagram with NaPSiRoKo mnemonic.""" fig, ax = plt.subplots(figsize=(8.27, 6)) ax.set_xlim(0, 10) ax.set_ylim(0, 8) @@ -133,11 +164,17 @@ def generate_pattern_template() -> None: ( "Si", "SIŁY (forces)", - "Konkurencyjne wymagania do pogodzenia\n(np. testowalność vs wydajność)", + "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), + ( + "Ko", + "KONSEKWENCJE", + "Tradeoffs: co zyskujemy, co tracimy", + GRAY1, + ), ] band_x = card_x + 0.3 @@ -202,7 +239,14 @@ def generate_pattern_template() -> None: # 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) + 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( @@ -232,14 +276,14 @@ def generate_pattern_template() -> None: 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}") + _logger.info(" Saved: %s", out) # ============================================================ # 2. Catalog Classification Map # ============================================================ def generate_catalog_map() -> None: - """Generate catalog map.""" + """Generate catalog classification map diagram.""" fig, ax = plt.subplots(figsize=(8.27, 7)) ax.set_xlim(0, 12) ax.set_ylim(0, 9) @@ -247,13 +291,15 @@ def generate_catalog_map() -> None: 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", + "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) + # Y-axis: Scale (architectural -> design -> idiom) ax.text( 0.3, 7.8, @@ -286,7 +332,9 @@ def generate_catalog_map() -> None: va="center", fontstyle="italic", ) - ax.plot([0.15, 0.45], [sy, sy], color=GRAY3, lw=0.8, ls="--") + ax.plot( + [0.15, 0.45], [sy, sy], color=GRAY3, lw=0.8, ls="--" + ) # X-axis: Domain ax.text( @@ -305,16 +353,16 @@ def generate_catalog_map() -> None: arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN}, ) - # Catalog boxes positioned by scale x domain + # Catalog boxes positioned by scale and 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", + "1996 • Buschmann\nLayers, Broker,\n" + "Pipes & Filters, MVC", GRAY1, "P", ), @@ -324,7 +372,8 @@ def generate_catalog_map() -> None: 2.5, 1.4, "GoF", - "1994 • Gamma et al.\n23 wzorce:\n5 kreac. / 7 strukt. / 11 behaw.", + "1994 • Gamma et al.\n23 wzorce:\n" + "5 kreac. / 7 strukt. / 11 behaw.", GRAY2, "G", ), @@ -334,7 +383,8 @@ def generate_catalog_map() -> None: 2.5, 1.4, "EIP", - "2003 • Hohpe & Woolf\nMessage Channel,\nRouter, Aggregator", + "2003 • Hohpe & Woolf\nMessage Channel,\n" + "Router, Aggregator", GRAY1, "E", ), @@ -344,7 +394,8 @@ def generate_catalog_map() -> None: 2.5, 1.4, "PoEAA", - "2002 • M. Fowler\nRepository, Unit of Work,\nDomain Model", + "2002 • M. Fowler\nRepository," + " Unit of Work,\nDomain Model", "white", "P", ), @@ -354,7 +405,8 @@ def generate_catalog_map() -> None: 2.8, 1.4, "Cloud\nPatterns", - "~2015 • Azure/AWS\nCircuit Breaker,\nSaga, Sidecar", + "~2015 • Azure/AWS\nCircuit Breaker,\n" + "Saga, Sidecar", GRAY1, "C", ), @@ -392,7 +444,11 @@ def generate_catalog_map() -> None: # Mnemonic letter in corner circle = plt.Circle( - (cx + 0.25, cy + ch - 0.25), 0.2, lw=1, edgecolor=LN, facecolor=GRAY5 + (cx + 0.25, cy + ch - 0.25), + 0.2, + lw=1, + edgecolor=LN, + facecolor=GRAY5, ) ax.add_patch(circle) ax.text( @@ -444,14 +500,14 @@ def generate_catalog_map() -> None: 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}") + _logger.info(" Saved: %s", out) # ============================================================ # 3. Three Pillars of Cataloguing # ============================================================ def generate_three_pillars() -> None: - """Generate three pillars.""" + """Generate three pillars of cataloguing diagram.""" fig, ax = plt.subplots(figsize=(8.27, 5.5)) ax.set_xlim(0, 12) ax.set_ylim(0, 7) @@ -467,7 +523,13 @@ def generate_three_pillars() -> None: # 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) + roof = plt.Polygon( + roof_pts, + closed=True, + lw=2, + edgecolor=LN, + facecolor=GRAY4, + ) ax.add_patch(roof) ax.text( 6, @@ -484,20 +546,29 @@ def generate_three_pillars() -> None: ( 1.3, "1. SZABLON\nOPISU", - "Każdy wzorzec ma\nte same pola:\nNazwa → Problem\n→ Siły → Rozwiązanie\n→ Konsekwencje", + "Każdy wzorzec ma\nte same pola:\n" + "Nazwa → 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", + "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", + "Wzorce referują się\nwzajemnie tworząc\n" + "sieć/graf:\nA → wymaga → B\n" + "B → wariant → C", + "Analogia:\n\u201ezobacz te\u017c\u201d\n" + "w encyklopedii", ), ] @@ -530,7 +601,10 @@ def generate_three_pillars() -> None: # 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 + [px + 0.2, px + pw - 0.2], + [py + ph - 1.0, py + ph - 1.0], + color=LN, + lw=0.8, ) # Description @@ -570,14 +644,19 @@ def generate_three_pillars() -> None: 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}") + _logger.info(" Saved: %s", out) # ============================================================ # 4. Filled-in Observer Pattern Card # ============================================================ +def _get_observer_band_height(index: int) -> float: + """Return band height for the given field index.""" + return _BAND_HEIGHTS[index] + + def generate_observer_card_filled() -> None: - """Generate observer card filled.""" + """Generate filled-in Observer pattern card diagram.""" fig, ax = plt.subplots(figsize=(8.27, 8.5)) ax.set_xlim(0, 10) ax.set_ylim(0, 10) @@ -610,7 +689,8 @@ def generate_observer_card_filled() -> None: ( "P", "PROBLEM", - "Obiekt (Subject) zmienia stan → wielu zależnych\n" + "Obiekt (Subject) zmienia stan → wielu" + " zależnych\n" "obiektów musi zareagować, ale Subject nie\n" "powinien znać ich konkretnych typów.", GRAY1, @@ -619,9 +699,12 @@ def generate_observer_card_filled() -> None: ( "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", + "• 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, ), @@ -651,21 +734,13 @@ def generate_observer_card_filled() -> None: 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 + for i, (abbr, title, content, fill, is_title_field) in enumerate( + fields + ): + band_h = _get_observer_band_height(i) 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) + _get_observer_band_height(j) + 0.15 for j in range(i) ) # Abbreviation circle @@ -734,13 +809,24 @@ def generate_observer_card_filled() -> None: # 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) + 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", + "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( @@ -758,14 +844,14 @@ def generate_observer_card_filled() -> None: 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}") + _logger.info(" Saved: %s", out) # ============================================================ # 5. Pattern Language Navigation Graph # ============================================================ def generate_pattern_language_navigation() -> None: - """Generate pattern language navigation.""" + """Generate pattern language navigation graph diagram.""" fig, ax = plt.subplots(figsize=(8.27, 9)) ax.set_xlim(0, 12) ax.set_ylim(0, 12) @@ -773,22 +859,37 @@ def generate_pattern_language_navigation() -> None: ax.axis("off") fig.patch.set_facecolor(BG) ax.set_title( - 'Język wzorców — nawigacja „problem → wzorzec → nowy problem"', + "Język wzorców \u2014 nawigacja" + " \u201eproblem \u2192 wzorzec" + " \u2192 nowy problem\u201d", 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) + ( + 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", + ), (7.0, 9.3, "Microservices", True, GRAY2), (7.0, 7.0, "API Gateway", True, GRAY2), (7.0, 4.7, "Circuit Breaker", True, GRAY2), @@ -816,7 +917,13 @@ def generate_pattern_language_navigation() -> None: ) ax.add_patch(rect) ax.text( - nx, ny, label, ha="center", va="center", fontsize=10, fontweight="bold" + nx, + ny, + label, + ha="center", + va="center", + fontsize=10, + fontweight="bold", ) else: w, h = node_w_prob, node_h_prob @@ -841,9 +948,8 @@ def generate_pattern_language_navigation() -> None: fontstyle="italic", ) - # Arrows: problem → pattern (solid), pattern → problem (dashed label) + # Arrows: problem -> pattern, pattern -> problem 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), @@ -851,9 +957,7 @@ def generate_pattern_language_navigation() -> None: (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), ] @@ -889,7 +993,6 @@ def generate_pattern_language_navigation() -> None: # Legend legend_y = 0.3 - # Problem node r1 = FancyBboxPatch( (1.0, legend_y - 0.2), 1.5, @@ -901,8 +1004,10 @@ def generate_pattern_language_navigation() -> None: linestyle="--", ) ax.add_patch(r1) - ax.text(1.75, legend_y, "Problem", ha="center", va="center", fontsize=7) - # Pattern node + ax.text( + 1.75, legend_y, "Problem", + ha="center", va="center", fontsize=7, + ) r2 = FancyBboxPatch( (3.5, legend_y - 0.2), 1.5, @@ -925,7 +1030,8 @@ def generate_pattern_language_navigation() -> None: ax.text( 6.5, legend_y, - "Nawigacja: Problem → Wzorzec → Nowy Problem → Wzorzec → ...", + "Nawigacja: Problem \u2192 Wzorzec" + " \u2192 Nowy Problem \u2192 Wzorzec \u2192 ...", ha="left", va="center", fontsize=7, @@ -933,20 +1039,23 @@ def generate_pattern_language_navigation() -> None: ) fig.tight_layout() - out = str(Path(OUTPUT_DIR) / "q14_pattern_language_navigation.png") + 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}") + _logger.info(" Saved: %s", out) # ============================================================ # Main # ============================================================ if __name__ == "__main__": - print("Generating PYTANIE 14 diagrams...") + logging.basicConfig(level=logging.INFO) + _logger.info("Generating PYTANIE 14 diagrams...") generate_pattern_template() generate_catalog_map() generate_three_pillars() generate_observer_card_filled() generate_pattern_language_navigation() - print("Done!") + _logger.info("Done!") diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_q31_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_q31_diagrams.py index 51bdb75..bbf0133 100755 --- a/python_pkg/praca_magisterska_video/generate_images/generate_q31_diagrams.py +++ b/python_pkg/praca_magisterska_video/generate_images/generate_q31_diagrams.py @@ -1,17 +1,24 @@ #!/usr/bin/env python3 -"""Generate diagrams for PYTANIE 31: Interaktywne wspomaganie decyzji w warunkach ryzyka. +"""Generate diagrams for PYTANIE 31. + +Interaktywne wspomaganie decyzji w warunkach ryzyka. Diagrams: 1. Payoff matrix + all criteria results comparison (bar chart) 2. Regret matrix construction step-by-step - 3. Hurwicz \u03b1 interpolation between maximax and maximin + 3. Hurwicz alpha interpolation between maximax and maximin 4. Decision criteria mnemonic map 5. Expected value criterion with probability-weighted bars - 6. Decision conditions spectrum (pewność → ryzyko → niepewność) + 6. Decision conditions spectrum All: A4-compatible, B&W, 300 DPI, laser-printer-friendly. """ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + import matplotlib as mpl mpl.use("Agg") @@ -22,6 +29,11 @@ from matplotlib.patches import FancyBboxPatch import matplotlib.pyplot as plt import numpy as np +if TYPE_CHECKING: + from matplotlib.axes import Axes + +_logger = logging.getLogger(__name__) + DPI = 300 BG = "white" LN = "black" @@ -36,29 +48,45 @@ GRAY3 = "#B8B8B8" GRAY4 = "#F5F5F5" GRAY5 = "#C0C0C0" +# Number of regret table header columns before the max-regret column +_REGRET_HEADER_COLS = 4 +# Number of data state columns +_DATA_STATE_COLS = 3 +# Expected-value for the winning alternative +_WINNING_EV = 95 + def draw_box( - ax, - x, - y, - w, - h, - text, - fill="white", - lw=1.2, - fontsize=FS, - fontweight="normal", - ha="center", - va="center", - rounded=True, + ax: Axes, + x: float, + y: float, + w: float, + h: float, + text: str, + *, + fill: str = "white", + lw: float = 1.2, + fontsize: float = FS, + fontweight: str = "normal", + ha: str = "center", + va: str = "center", + rounded: bool = True, ) -> None: - """Draw box.""" + """Draw a labeled box on the axes.""" if rounded: rect = FancyBboxPatch( - (x, y), w, h, boxstyle="round,pad=0.05", lw=lw, edgecolor=LN, facecolor=fill + (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) + rect = mpatches.Rectangle( + (x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill + ) ax.add_patch(rect) ax.text( x + w / 2, @@ -72,35 +100,45 @@ def draw_box( ) -def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None: - """Draw arrow.""" +def draw_arrow( + ax: Axes, + x1: float, + y1: float, + x2: float, + y2: float, + *, + lw: float = 1.2, + style: str = "->", + color: str = LN, +) -> None: + """Draw an arrow between two points.""" ax.annotate( "", xy=(x2, y2), xytext=(x1, y1), - arrowprops={"arrowstyle": style, "color": color, "lw": lw}, + arrowprops={ + "arrowstyle": style, + "color": color, + "lw": lw, + }, ) # ============================================================ # 1. PAYOFF MATRIX + ALL CRITERIA BAR CHART # ============================================================ -def draw_criteria_comparison() -> None: - """Draw criteria comparison.""" - fig, axes = plt.subplots( - 1, 2, figsize=(8.27, 4.5), gridspec_kw={"width_ratios": [1.2, 1]} - ) - - # -- Left: Payoff matrix as styled table -- - ax = axes[0] +def _draw_payoff_table(ax: Axes) -> None: + """Draw the payoff matrix table on the left panel.""" ax.axis("off") ax.set_xlim(0, 6) ax.set_ylim(0, 6) ax.set_title( - "Macierz wypłat (tys. zł)", fontsize=FS_TITLE, fontweight="bold", pad=8 + "Macierz wypłat (tys. zł)", + fontsize=FS_TITLE, + fontweight="bold", + pad=8, ) - # Headers headers_col = ["", "S₁\n(dobra)", "S₂\n(średnia)", "S₃\n(zła)"] rows = [ ["A₁ (fabryka)", "200", "50", "-100"], @@ -118,7 +156,12 @@ def draw_criteria_comparison() -> None: for j, h in enumerate(headers_col): fill = GRAY2 if j > 0 else GRAY3 rect = mpatches.Rectangle( - (x, start_y), col_w[j], row_h, lw=1, edgecolor=LN, facecolor=fill + (x, start_y), + col_w[j], + row_h, + lw=1, + edgecolor=LN, + facecolor=fill, ) ax.add_patch(rect) ax.text( @@ -137,12 +180,18 @@ def draw_criteria_comparison() -> None: x = start_x y = start_y - (i + 1) * row_h for j, val in enumerate(row): - fill = GRAY4 if j == 0 else ("white" if i % 2 == 0 else GRAY1) - # Highlight negative + fill = GRAY4 if j == 0 else ( + "white" if i % 2 == 0 else GRAY1 + ) if val.startswith("-"): fill = "#D8D8D8" rect = mpatches.Rectangle( - (x, y), col_w[j], row_h, lw=1, edgecolor=LN, facecolor=fill + (x, y), + col_w[j], + row_h, + lw=1, + edgecolor=LN, + facecolor=fill, ) ax.add_patch(rect) fw = "bold" if j == 0 else "normal" @@ -164,7 +213,12 @@ def draw_criteria_comparison() -> None: for j, val in enumerate(probs): fill = GRAY5 if j > 0 else GRAY3 rect = mpatches.Rectangle( - (x, y), col_w[j], row_h * 0.7, lw=1, edgecolor=LN, facecolor=fill + (x, y), + col_w[j], + row_h * 0.7, + lw=1, + edgecolor=LN, + facecolor=fill, ) ax.add_patch(rect) ax.text( @@ -179,8 +233,9 @@ def draw_criteria_comparison() -> None: ) x += col_w[j] - # -- Right: Bar chart comparing criteria results -- - ax2 = axes[1] + +def _draw_criteria_bars(ax2: Axes) -> None: + """Draw the criteria comparison bar chart on the right panel.""" criteria = [ "E[X]", "Laplace", @@ -190,33 +245,36 @@ def draw_criteria_comparison() -> None: "Savage", ] - # Recalculate with probabilities 0.5, 0.3, 0.2 - # E[X]: A1=200*0.5+50*0.3+(-100)*0.2=100+15-20=95 - # A2=80*0.5+70*0.3+40*0.2=40+21+8=69 - # A3=30*0.5+30*0.3+30*0.2=15+9+6=30 ev = [95, 69, 30] laplace = [50, 63.3, 30] maximax = [200, 80, 30] maximin = [-100, 40, 30] - hurwicz = [80, 64, 30] # alpha=0.6 - savage_maxregret = [140, 120, 170] # lower = better + hurwicz = [80, 64, 30] + savage_maxregret = [140, 120, 170] - # Which alternative wins for each criterion? - winners = [0, 1, 0, 1, 0, 1] # index of winning alternative + winners = [0, 1, 0, 1, 0, 1] - # Display as grouped bar chart - each criterion shows the 3 alternatives x_pos = np.arange(len(criteria)) width = 0.22 hatches = ["///", "...", "xxx"] labels = ["A₁ (fabryka)", "A₂ (sklep)", "A₃ (obligacje)"] all_vals = [ - [ev[0], laplace[0], maximax[0], maximin[0], hurwicz[0], savage_maxregret[0]], - [ev[1], laplace[1], maximax[1], maximin[1], hurwicz[1], savage_maxregret[1]], - [ev[2], laplace[2], maximax[2], maximin[2], hurwicz[2], savage_maxregret[2]], + [ + ev[0], laplace[0], maximax[0], + maximin[0], hurwicz[0], savage_maxregret[0], + ], + [ + ev[1], laplace[1], maximax[1], + maximin[1], hurwicz[1], savage_maxregret[1], + ], + [ + ev[2], laplace[2], maximax[2], + maximin[2], hurwicz[2], savage_maxregret[2], + ], ] - for i in range(3): + for i in range(_DATA_STATE_COLS): ax2.bar( x_pos + (i - 1) * width, all_vals[i], @@ -228,7 +286,6 @@ def draw_criteria_comparison() -> None: lw=0.8, ) - # Mark winners with star for c_idx in range(len(criteria)): w = winners[c_idx] val = all_vals[w][c_idx] @@ -245,14 +302,18 @@ def draw_criteria_comparison() -> None: ax2.set_xticks(x_pos) ax2.set_xticklabels(criteria, fontsize=7) ax2.set_ylabel("Wartość kryterium", fontsize=8) - ax2.set_title("Porównanie kryteriów", fontsize=FS_TITLE, fontweight="bold", pad=8) + ax2.set_title( + "Porównanie kryteriów", + fontsize=FS_TITLE, + fontweight="bold", + pad=8, + ) ax2.legend(fontsize=7, loc="upper right") ax2.axhline(y=0, color=LN, lw=0.5, ls="-") ax2.spines["top"].set_visible(False) ax2.spines["right"].set_visible(False) ax2.tick_params(labelsize=7) - # Note about Savage ax2.text( 5, -30, @@ -263,30 +324,35 @@ def draw_criteria_comparison() -> None: style="italic", ) + +def draw_criteria_comparison() -> None: + """Draw payoff matrix and criteria comparison chart.""" + fig, axes = plt.subplots( + 1, + 2, + figsize=(8.27, 4.5), + gridspec_kw={"width_ratios": [1.2, 1]}, + ) + + _draw_payoff_table(axes[0]) + _draw_criteria_bars(axes[1]) + plt.tight_layout() outpath = str(Path(OUTPUT_DIR) / "q31_criteria_comparison.png") fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG) plt.close(fig) - print(f" Saved: {outpath}") + _logger.info(" Saved: %s", outpath) # ============================================================ # 2. REGRET MATRIX CONSTRUCTION # ============================================================ -def draw_regret_matrix() -> None: - """Draw regret matrix.""" - fig, ax = plt.subplots(1, 1, figsize=(8.27, 5)) - ax.axis("off") - ax.set_xlim(0, 10) - ax.set_ylim(0, 7) - ax.set_title( - "Kryterium Savage'a — budowa macierzy żalu", - fontsize=FS_TITLE + 1, - fontweight="bold", - pad=10, - ) - - # --- Step 1: Original payoff matrix (left) --- +def _draw_original_payoff( + ax: Axes, + start_y: float, + row_h: float, +) -> None: + """Draw the original payoff matrix (left side of regret fig).""" ax.text( 2.2, 6.3, @@ -298,7 +364,6 @@ def draw_regret_matrix() -> None: ) col_w = 1.0 - row_h = 0.55 headers = ["", "S₁", "S₂", "S₃"] data = [ ["A₁", "200", "50", "-100"], @@ -306,13 +371,17 @@ def draw_regret_matrix() -> None: ["A₃", "30", "30", "30"], ] start_x = 0.3 - start_y = 5.5 for j, h in enumerate(headers): w = 0.7 if j == 0 else col_w x = start_x + (0 if j == 0 else 0.7 + (j - 1) * col_w) rect = mpatches.Rectangle( - (x, start_y), w, row_h, lw=1, edgecolor=LN, facecolor=GRAY2 + (x, start_y), + w, + row_h, + lw=1, + edgecolor=LN, + facecolor=GRAY2, ) ax.add_patch(rect) ax.text( @@ -332,45 +401,37 @@ def draw_regret_matrix() -> None: x = start_x + (0 if j == 0 else 0.7 + (j - 1) * col_w) fill = GRAY4 if j == 0 else "white" rect = mpatches.Rectangle( - (x, y), w, row_h, lw=1, edgecolor=LN, facecolor=fill + (x, y), + w, + row_h, + lw=1, + edgecolor=LN, + facecolor=fill, ) ax.add_patch(rect) ax.text( - x + w / 2, y + row_h / 2, val, ha="center", va="center", fontsize=FS + x + w / 2, + y + row_h / 2, + val, + ha="center", + va="center", + fontsize=FS, ) # Max per column annotation - max_y = start_y - 3 * row_h - 0.1 - ax.text( - start_x + 0.7 + 0.5 * col_w, - max_y, - "max=200", - fontsize=7, - ha="center", - va="top", - fontweight="bold", - color="#333", - ) - ax.text( - start_x + 0.7 + 1.5 * col_w, - max_y, - "max=70", - fontsize=7, - ha="center", - va="top", - fontweight="bold", - color="#333", - ) - ax.text( - start_x + 0.7 + 2.5 * col_w, - max_y, - "max=40", - fontsize=7, - ha="center", - va="top", - fontweight="bold", - color="#333", - ) + max_y = start_y - _DATA_STATE_COLS * row_h - 0.1 + col_maxes = ["max=200", "max=70", "max=40"] + for idx, label in enumerate(col_maxes): + ax.text( + start_x + 0.7 + (idx + 0.5) * col_w, + max_y, + label, + fontsize=7, + ha="center", + va="top", + fontweight="bold", + color="#333", + ) # Arrow ax.annotate( @@ -389,7 +450,13 @@ def draw_regret_matrix() -> None: fontweight="bold", ) - # --- Step 2: Regret matrix (right) --- + +def _draw_regret_table( + ax: Axes, + start_y: float, + row_h: float, +) -> None: + """Draw the regret matrix (right side of regret fig).""" ax.text( 7.5, 6.3, @@ -409,21 +476,25 @@ def draw_regret_matrix() -> None: start_x2 = 5.3 for j, h in enumerate(headers2): - w = 0.7 if j == 0 else (0.9 if j < 4 else 1.0) + w = 0.7 if j == 0 else ( + 0.9 if j < _REGRET_HEADER_COLS else 1.0 + ) x = start_x2 if j == 0: x = start_x2 - elif j <= 3: + elif j <= _DATA_STATE_COLS: x = start_x2 + 0.7 + (j - 1) * 0.9 else: - x = start_x2 + 0.7 + 3 * 0.9 + x = start_x2 + 0.7 + _DATA_STATE_COLS * 0.9 rect = mpatches.Rectangle( (x, start_y), w, row_h, lw=1, edgecolor=LN, - facecolor=GRAY2 if j < 4 else GRAY3, + facecolor=( + GRAY2 if j < _REGRET_HEADER_COLS else GRAY3 + ), ) ax.add_patch(rect) ax.text( @@ -441,16 +512,26 @@ def draw_regret_matrix() -> None: y = start_y - (i + 1) * row_h for j, val in enumerate(row): w = 0.7 if j == 0 else 0.9 - x = start_x2 + (0 if j == 0 else 0.7 + (j - 1) * 0.9) + x = start_x2 + ( + 0 if j == 0 else 0.7 + (j - 1) * 0.9 + ) fill = GRAY4 if j == 0 else "white" - # Highlight the max regret cell if j > 0 and int(val) == max_regrets[i]: fill = GRAY2 rect = mpatches.Rectangle( - (x, y), w, row_h, lw=1, edgecolor=LN, facecolor=fill + (x, y), + w, + row_h, + lw=1, + edgecolor=LN, + facecolor=fill, ) ax.add_patch(rect) - fw = "bold" if (j > 0 and int(val) == max_regrets[i]) else "normal" + fw = ( + "bold" + if (j > 0 and int(val) == max_regrets[i]) + else "normal" + ) ax.text( x + w / 2, y + row_h / 2, @@ -462,19 +543,20 @@ def draw_regret_matrix() -> None: ) # Max regret column - x = start_x2 + 0.7 + 3 * 0.9 + x = start_x2 + 0.7 + _DATA_STATE_COLS * 0.9 w = 1.0 - fill = "#C8C8C8" if max_regrets[i] == min(max_regrets) else GRAY1 + is_winner = max_regrets[i] == min(max_regrets) + fill = "#C8C8C8" if is_winner else GRAY1 rect = mpatches.Rectangle( (x, y), w, row_h, - lw=1.5 if max_regrets[i] == min(max_regrets) else 1, + lw=1.5 if is_winner else 1, edgecolor=LN, facecolor=fill, ) ax.add_patch(rect) - marker = " ★" if max_regrets[i] == min(max_regrets) else "" + marker = " ★" if is_winner else "" ax.text( x + w / 2, y + row_h / 2, @@ -485,11 +567,32 @@ def draw_regret_matrix() -> None: fontweight="bold", ) + +def draw_regret_matrix() -> None: + """Draw the regret matrix construction diagram.""" + fig, ax = plt.subplots(1, 1, figsize=(8.27, 5)) + ax.axis("off") + ax.set_xlim(0, 10) + ax.set_ylim(0, 7) + ax.set_title( + "Kryterium Savage'a \u2014 budowa macierzy żalu", + fontsize=FS_TITLE + 1, + fontweight="bold", + pad=10, + ) + + start_y = 5.5 + row_h = 0.55 + + _draw_original_payoff(ax, start_y, row_h) + _draw_regret_table(ax, start_y, row_h) + # Bottom conclusion ax.text( 5.0, 2.8, - "Krok 3: Wybierz min z max żalu → A₂ (max żal = 120)", + "Krok 3: Wybierz min z max żalu" + " → A₂ (max żal = 120)", fontsize=10, ha="center", va="center", @@ -502,13 +605,15 @@ def draw_regret_matrix() -> None: }, ) - # Interpretatation examples + # Interpretation examples ax.text( 5.0, 2.0, "Interpretacja żalu: r₁₃ = 140 oznacza:\n" - "„Gdyby nastąpił S₃ (zła koniunktura), a wybrałbym A₁,\n" - 'żałowałbym, bo najlepszą opcją byłoby A₂ z wynikiem 40 — traciłbym 140"', + "\u201eGdyby nastąpił S₃ (zła koniunktura)," + " a wybrałbym A₁,\n" + "żałowałbym, bo najlepszą opcją byłoby" + " A₂ z wynikiem 40 \u2014 traciłbym 140\u201d", fontsize=7.5, ha="center", va="center", @@ -525,8 +630,9 @@ def draw_regret_matrix() -> None: ax.text( 5.0, 0.8, - 'Mnemonik: Savage = „Żal jak nóż"\nMaksymalny żal to nóż ' - "— wybierz opcję z NAJMNIEJSZYM nożem", + "Mnemonik: Savage = \u201eŻal jak nóż\u201d\n" + "Maksymalny żal to nóż " + "\u2014 wybierz opcję z NAJMNIEJSZYM nożem", fontsize=8, ha="center", va="center", @@ -543,17 +649,18 @@ def draw_regret_matrix() -> None: outpath = str(Path(OUTPUT_DIR) / "q31_regret_matrix.png") fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG) plt.close(fig) - print(f" Saved: {outpath}") + _logger.info(" Saved: %s", outpath) # ============================================================ # 3. HURWICZ alpha INTERPOLATION # ============================================================ def draw_hurwicz_interpolation() -> None: - """Draw hurwicz interpolation.""" + """Draw Hurwicz alpha interpolation diagram.""" fig, ax = plt.subplots(1, 1, figsize=(8.27, 4)) ax.set_title( - "Kryterium Hurwicza — wpływ \u03b1 na wybór alternatywy", + "Kryterium Hurwicza" + " \u2014 wpływ \u03b1 na wybór alternatywy", fontsize=FS_TITLE + 1, fontweight="bold", pad=10, @@ -561,26 +668,27 @@ def draw_hurwicz_interpolation() -> None: alphas = np.linspace(0, 1, 200) - # V(Ai) = alpha * max_i + (1-alpha) * min_i - # A1: max=200, min=-100 - # A2: max=80, min=40 - # A3: max=30, min=30 v1 = alphas * 200 + (1 - alphas) * (-100) v2 = alphas * 80 + (1 - alphas) * 40 v3 = alphas * 30 + (1 - alphas) * 30 - ax.plot(alphas, v1, "k-", lw=2, label="A₁ (fabryka): V = 300\u03b1 - 100") - ax.plot(alphas, v2, "k--", lw=2, label="A₂ (sklep): V = 40\u03b1 + 40") - ax.plot(alphas, v3, "k:", lw=2, label="A₃ (obligacje): V = 30") + ax.plot( + alphas, v1, "k-", lw=2, + label="A₁ (fabryka): V = 300\u03b1 - 100", + ) + ax.plot( + alphas, v2, "k--", lw=2, + label="A₂ (sklep): V = 40\u03b1 + 40", + ) + ax.plot( + alphas, v3, "k:", lw=2, + label="A₃ (obligacje): V = 30", + ) - # Find crossover points - # A2 = A1: 40alpha + 40 = 300alpha - 100 → 140 = 260alpha → alpha = 140/260 ≈ 0.538 + # Crossover A2=A1 alpha_cross_12 = 140 / 260 v_cross_12 = 40 * alpha_cross_12 + 40 - # A2 = A3: 40alpha + 40 = 30 → 40alpha = -10 → alpha = -0.25 (never — A2 always > A3) - # A1 = A3: 300alpha - 100 = 30 → 300alpha = 130 → alpha = 130/300 ≈ 0.433 - ax.plot(alpha_cross_12, v_cross_12, "ko", markersize=8, zorder=5) ax.annotate( f"\u03b1 ≈ {alpha_cross_12:.2f}\nA₁ = A₂", @@ -588,12 +696,16 @@ def draw_hurwicz_interpolation() -> None: xytext=(alpha_cross_12 + 0.12, v_cross_12 - 30), fontsize=8, fontweight="bold", - arrowprops={"arrowstyle": "->", "color": LN, "lw": 1}, + arrowprops={ + "arrowstyle": "->", + "color": LN, + "lw": 1, + }, ) # Shade winning regions - ax.axvspan(0, alpha_cross_12, alpha=0.08, color="black", label="_") - ax.axvspan(alpha_cross_12, 1, alpha=0.15, color="black", label="_") + ax.axvspan(0, alpha_cross_12, alpha=0.08, color="black") + ax.axvspan(alpha_cross_12, 1, alpha=0.15, color="black") ax.text( alpha_cross_12 / 2, @@ -602,7 +714,11 @@ def draw_hurwicz_interpolation() -> None: fontsize=8, ha="center", va="center", - bbox={"boxstyle": "round", "facecolor": "white", "edgecolor": LN}, + bbox={ + "boxstyle": "round", + "facecolor": "white", + "edgecolor": LN, + }, ) ax.text( (alpha_cross_12 + 1) / 2, @@ -611,7 +727,11 @@ def draw_hurwicz_interpolation() -> None: fontsize=8, ha="center", va="center", - bbox={"boxstyle": "round", "facecolor": "white", "edgecolor": LN}, + bbox={ + "boxstyle": "round", + "facecolor": "white", + "edgecolor": LN, + }, ) # Special alpha values @@ -637,7 +757,9 @@ def draw_hurwicz_interpolation() -> None: ) ax.set_xlabel("Współczynnik optymizmu \u03b1", fontsize=9) - ax.set_ylabel("V(Aᵢ) = \u03b1·max + (1-\u03b1)·min", fontsize=9) + ax.set_ylabel( + "V(Aᵢ) = \u03b1·max + (1-\u03b1)·min", fontsize=9 + ) ax.legend(fontsize=8, loc="upper left") ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) @@ -649,21 +771,21 @@ def draw_hurwicz_interpolation() -> None: outpath = str(Path(OUTPUT_DIR) / "q31_hurwicz_alpha.png") fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG) plt.close(fig) - print(f" Saved: {outpath}") + _logger.info(" Saved: %s", outpath) # ============================================================ # 4. DECISION CRITERIA MNEMONIC MAP # ============================================================ def draw_criteria_mnemonic() -> None: - """Draw criteria mnemonic.""" + """Draw decision criteria mnemonic map diagram.""" fig, ax = plt.subplots(1, 1, figsize=(8.27, 6)) ax.set_xlim(0, 10) ax.set_ylim(0, 8) ax.set_aspect("equal") ax.axis("off") ax.set_title( - "Mapa mnemoniczna — 6 kryteriów decyzyjnych", + "Mapa mnemoniczna \u2014 6 kryteriów decyzyjnych", fontsize=FS_TITLE + 2, fontweight="bold", pad=10, @@ -685,34 +807,49 @@ def draw_criteria_mnemonic() -> None: # Criteria boxes around the center criteria = [ - # (x, y, w, h, title, mnemonic, formula) ( - 0, - 6.5, - 3, - 1.2, + 0, 6.5, 3, 1.2, "WARTOŚĆ OCZEKIWANA", - '„Mam prawdopodobieństwa"', + "\u201eMam prawdopodobieństwa\u201d", "E[Aᵢ] = Σ pⱼ·aᵢⱼ", ), - (3.5, 6.5, 3, 1.2, "LAPLACE", '„Wszystko po równo"', "V = Σaᵢⱼ / n"), - (7, 6.5, 3, 1.2, "MAXIMAX", '„Optymista: max z max"', "max maxⱼ aᵢⱼ"), - (0, 0.5, 3, 1.2, "MAXIMIN (Wald)", '„Pesymista: max z min"', "max minⱼ aᵢⱼ"), ( - 3.5, - 0.5, - 3, - 1.2, + 3.5, 6.5, 3, 1.2, + "LAPLACE", + "\u201eWszystko po równo\u201d", + "V = Σaᵢⱼ / n", + ), + ( + 7, 6.5, 3, 1.2, + "MAXIMAX", + "\u201eOptymista: max z max\u201d", + "max maxⱼ aᵢⱼ", + ), + ( + 0, 0.5, 3, 1.2, + "MAXIMIN (Wald)", + "\u201ePesymista: max z min\u201d", + "max minⱼ aᵢⱼ", + ), + ( + 3.5, 0.5, 3, 1.2, "HURWICZ", - '„\u03b1 pomiędzy"', + "\u201e\u03b1 pomiędzy\u201d", "\u03b1·max + (1-\u03b1)·min", ), - (7, 0.5, 3, 1.2, "SAVAGE", '„Min max żalu"', "min maxⱼ rᵢⱼ"), + ( + 7, 0.5, 3, 1.2, + "SAVAGE", + "\u201eMin max żalu\u201d", + "min maxⱼ rᵢⱼ", + ), ] fills = [GRAY3, GRAY1, "white", "white", GRAY1, GRAY3] - for i, (x, y, w, h, title, mnem, formula) in enumerate(criteria): + for i, (x, y, w, h, title, mnem, formula) in enumerate( + criteria + ): rect = FancyBboxPatch( (x, y), w, @@ -753,9 +890,10 @@ def draw_criteria_mnemonic() -> None: ) # Arrows from center to each box - cx, cy = 5, 4 # center of macierz - bx, by = x + w / 2, y + h / 2 - if by > cy: + cx, cy = 5, 4 + bx = x + w / 2 + by_center = y + h / 2 + if by_center > cy: ax.annotate( "", xy=(bx, y), @@ -781,112 +919,51 @@ def draw_criteria_mnemonic() -> None: ) # Labels on arrows - ax.text( - 1.2, - 5.6, - "znane p", - fontsize=7, - ha="center", - va="center", - bbox={ - "boxstyle": "round,pad=0.15", - "facecolor": "white", - "edgecolor": GRAY3, - "lw": 0.5, - }, - ) - ax.text( - 5, - 5.6, - "p = 1/n", - fontsize=7, - ha="center", - va="center", - bbox={ - "boxstyle": "round,pad=0.15", - "facecolor": "white", - "edgecolor": GRAY3, - "lw": 0.5, - }, - ) - ax.text( - 8.7, - 5.6, - "max ↑", - fontsize=7, - ha="center", - va="center", - bbox={ - "boxstyle": "round,pad=0.15", - "facecolor": "white", - "edgecolor": GRAY3, - "lw": 0.5, - }, - ) - ax.text( - 1.2, - 2.5, - "min ↑", - fontsize=7, - ha="center", - va="center", - bbox={ - "boxstyle": "round,pad=0.15", - "facecolor": "white", - "edgecolor": GRAY3, - "lw": 0.5, - }, - ) - ax.text( - 5, - 2.5, - "podaj \u03b1", - fontsize=7, - ha="center", - va="center", - bbox={ - "boxstyle": "round,pad=0.15", - "facecolor": "white", - "edgecolor": GRAY3, - "lw": 0.5, - }, - ) - ax.text( - 8.7, - 2.5, - "macierz\nżalu", - fontsize=7, - ha="center", - va="center", - bbox={ - "boxstyle": "round,pad=0.15", - "facecolor": "white", - "edgecolor": GRAY3, - "lw": 0.5, - }, - ) + arrow_labels = [ + (1.2, 5.6, "znane p"), + (5, 5.6, "p = 1/n"), + (8.7, 5.6, "max ↑"), + (1.2, 2.5, "min ↑"), + (5, 2.5, "podaj \u03b1"), + (8.7, 2.5, "macierz\nżalu"), + ] + for lx, ly, ltext in arrow_labels: + ax.text( + lx, + ly, + ltext, + fontsize=7, + ha="center", + va="center", + bbox={ + "boxstyle": "round,pad=0.15", + "facecolor": "white", + "edgecolor": GRAY3, + "lw": 0.5, + }, + ) plt.tight_layout() outpath = str(Path(OUTPUT_DIR) / "q31_criteria_mnemonic.png") fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG) plt.close(fig) - print(f" Saved: {outpath}") + _logger.info(" Saved: %s", outpath) # ============================================================ # 5. EXPECTED VALUE CRITERION WITH PROBABILITY BARS # ============================================================ def draw_expected_value() -> None: - """Draw expected value.""" + """Draw expected value criterion with probability-weighted bars.""" fig, axes = plt.subplots(1, 3, figsize=(8.27, 3.5), sharey=True) fig.suptitle( - "Kryterium wartości oczekiwanej E[X] — rozkład wyników per alternatywa", + "Kryterium wartości oczekiwanej E[X]" + " \u2014 rozkład wyników per alternatywa", fontsize=FS_TITLE, fontweight="bold", y=1.02, ) - # Probabilities: p1=0.5, p2=0.3, p3=0.2 probs = [0.5, 0.3, 0.2] alts = [ ("A₁ (fabryka)", [200, 50, -100], 95), @@ -896,12 +973,15 @@ def draw_expected_value() -> None: hatches = ["///", "...", "xxx"] - for _idx, (ax, (name, vals, ev)) in enumerate(zip(axes, alts, strict=False)): - # Bar: height = payoff, width proportional to probability + for _idx, (ax, (name, vals, ev)) in enumerate( + zip(axes, alts, strict=False) + ): x_positions = [0, 0.6, 1.0] widths = [p * 0.9 for p in probs] - for i, (v, p, h) in enumerate(zip(vals, probs, hatches, strict=False)): + for i, (v, p, h) in enumerate( + zip(vals, probs, hatches, strict=False) + ): color = "white" if v >= 0 else GRAY2 ax.bar( x_positions[i], @@ -913,7 +993,6 @@ def draw_expected_value() -> None: lw=0.8, align="edge", ) - # Value label offset = 8 if v >= 0 else -12 ax.text( x_positions[i] + widths[i] / 2, @@ -924,7 +1003,6 @@ def draw_expected_value() -> None: fontsize=8, fontweight="bold", ) - # Probability contribution contrib = v * p ax.text( x_positions[i] + widths[i] / 2, @@ -946,7 +1024,11 @@ def draw_expected_value() -> None: fontweight="bold", va="center", ha="left", - bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY1, "edgecolor": LN}, + bbox={ + "boxstyle": "round,pad=0.15", + "facecolor": GRAY1, + "edgecolor": LN, + }, ) ax.set_title(name, fontsize=9, fontweight="bold") @@ -958,7 +1040,7 @@ def draw_expected_value() -> None: ax.tick_params(labelsize=7) # Star on winner - if ev == 95: + if ev == _WINNING_EV: ax.text( 0.7, ev + 20, @@ -975,21 +1057,22 @@ def draw_expected_value() -> None: outpath = str(Path(OUTPUT_DIR) / "q31_expected_value.png") fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG) plt.close(fig) - print(f" Saved: {outpath}") + _logger.info(" Saved: %s", outpath) # ============================================================ # 6. DECISION CONDITIONS SPECTRUM # ============================================================ def draw_conditions_spectrum() -> None: - """Draw conditions spectrum.""" + """Draw decision conditions spectrum diagram.""" fig, ax = plt.subplots(1, 1, figsize=(8.27, 3.5)) ax.set_xlim(0, 10) ax.set_ylim(0, 5) ax.set_aspect("equal") ax.axis("off") ax.set_title( - "Warunki decyzyjne — spektrum wiedzy decydenta", + "Warunki decyzyjne" + " \u2014 spektrum wiedzy decydenta", fontsize=FS_TITLE + 1, fontweight="bold", pad=10, @@ -998,12 +1081,8 @@ def draw_conditions_spectrum() -> None: # Three zones zones = [ ( - 0.3, - 1.5, - 2.8, - 2.5, - "PEWNOŚĆ", - "white", + 0.3, 1.5, 2.8, 2.5, + "PEWNOŚĆ", "white", [ "Znamy dokładny wynik", "Przykład: lokata 5%", @@ -1012,12 +1091,8 @@ def draw_conditions_spectrum() -> None: ], ), ( - 3.5, - 1.5, - 2.8, - 2.5, - "RYZYKO", - GRAY1, + 3.5, 1.5, 2.8, 2.5, + "RYZYKO", GRAY1, [ "Znamy wyniki I prawdop.", "Przykład: gra w kości", @@ -1026,12 +1101,8 @@ def draw_conditions_spectrum() -> None: ], ), ( - 6.7, - 1.5, - 2.8, - 2.5, - "NIEPEWNOŚĆ", - GRAY3, + 6.7, 1.5, 2.8, 2.5, + "NIEPEWNOŚĆ", GRAY3, [ "Znamy wyniki, ale", "NIE znamy prawdop.", @@ -1043,7 +1114,13 @@ def draw_conditions_spectrum() -> None: for x, y, w, h, title, fill, lines in zones: rect = FancyBboxPatch( - (x, y), w, h, boxstyle="round,pad=0.1", lw=2, edgecolor=LN, facecolor=fill + (x, y), + w, + h, + boxstyle="round,pad=0.1", + lw=2, + edgecolor=LN, + facecolor=fill, ) ax.add_patch(rect) ax.text( @@ -1088,17 +1165,32 @@ def draw_conditions_spectrum() -> None: w = 9.2 / n_steps + 0.01 gray_val = 1 - (i / n_steps) * 0.7 rect = mpatches.Rectangle( - (x, gradient_y), w, gradient_h, lw=0, facecolor=str(gray_val) + (x, gradient_y), + w, + gradient_h, + lw=0, + facecolor=str(gray_val), ) ax.add_patch(rect) rect = mpatches.Rectangle( - (0.3, gradient_y), 9.2, gradient_h, lw=1.5, edgecolor=LN, facecolor="none" + (0.3, gradient_y), + 9.2, + gradient_h, + lw=1.5, + edgecolor=LN, + facecolor="none", ) ax.add_patch(rect) - ax.text(0.3, gradient_y - 0.15, "Dużo wiedzy", fontsize=7, ha="left", va="top") - ax.text(9.5, gradient_y - 0.15, "Mało wiedzy", fontsize=7, ha="right", va="top") + ax.text( + 0.3, gradient_y - 0.15, + "Dużo wiedzy", fontsize=7, ha="left", va="top", + ) + ax.text( + 9.5, gradient_y - 0.15, + "Mało wiedzy", fontsize=7, ha="right", va="top", + ) ax.text( 4.95, gradient_y + gradient_h / 2, @@ -1114,18 +1206,19 @@ def draw_conditions_spectrum() -> None: outpath = str(Path(OUTPUT_DIR) / "q31_conditions_spectrum.png") fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG) plt.close(fig) - print(f" Saved: {outpath}") + _logger.info(" Saved: %s", outpath) # ============================================================ # MAIN # ============================================================ if __name__ == "__main__": - print("Generating PYTANIE 31 diagrams...") + logging.basicConfig(level=logging.INFO) + _logger.info("Generating PYTANIE 31 diagrams...") draw_criteria_comparison() draw_regret_matrix() draw_hurwicz_interpolation() draw_criteria_mnemonic() draw_expected_value() draw_conditions_spectrum() - print("Done! All Q31 diagrams saved to:", OUTPUT_DIR) + _logger.info("Done! All Q31 diagrams saved to: %s", OUTPUT_DIR) diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_robot_lang_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_robot_lang_diagrams.py index 4258cec..e9b21b7 100755 --- a/python_pkg/praca_magisterska_video/generate_images/generate_robot_lang_diagrams.py +++ b/python_pkg/praca_magisterska_video/generate_images/generate_robot_lang_diagrams.py @@ -11,6 +11,11 @@ Diagrams: 5. ROS architecture (pub/sub nodes) """ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + import matplotlib as mpl mpl.use("Agg") @@ -21,6 +26,11 @@ from matplotlib.patches import FancyBboxPatch import matplotlib.pyplot as plt import numpy as np +if TYPE_CHECKING: + from matplotlib.axes import Axes + +_logger = logging.getLogger(__name__) + DPI = 300 BG = "white" LN = "black" @@ -38,21 +48,38 @@ 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, + ax: Axes, + x: float, + y: float, + w: float, + h: float, + text: str, + *, + fill: str = "white", + lw: float = 1.2, + fontsize: float = FS, + fontweight: str = "normal", + ha: str = "center", + va: str = "center", + rounded: bool = True, ) -> None: - """Draw box.""" + """Draw a labeled box on the axes. + + Args: + ax: Matplotlib axes to draw on. + x: Left edge x-coordinate. + y: Bottom edge y-coordinate. + w: Box width. + h: Box height. + text: Label text inside the box. + fill: Fill color. + lw: Line width. + fontsize: Font size for label. + fontweight: Font weight for label. + ha: Horizontal alignment. + va: Vertical alignment. + rounded: Whether to use rounded corners. + """ if rounded: rect = FancyBboxPatch( (x, y), w, h, boxstyle="round,pad=0.05", lw=lw, edgecolor=LN, facecolor=fill @@ -72,8 +99,29 @@ def draw_box( ) -def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None: - """Draw arrow.""" +def draw_arrow( + ax: Axes, + x1: float, + y1: float, + x2: float, + y2: float, + *, + lw: float = 1.2, + style: str = "->", + color: str = LN, +) -> None: + """Draw an arrow annotation. + + Args: + ax: Matplotlib axes to draw on. + x1: Start x-coordinate. + y1: Start y-coordinate. + x2: End x-coordinate. + y2: End y-coordinate. + lw: Line width. + style: Arrow style. + color: Arrow color. + """ ax.annotate( "", xy=(x2, y2), @@ -101,7 +149,7 @@ def draw_trms_pyramid() -> None: # Pyramid layers (bottom to top) layers = [ - # (y, left_x, right_x, label, sublabel, fill, examples, timing) + # Fields: y left_x right_x label sublabel fill examples timing ( 0.5, 1.0, @@ -261,7 +309,7 @@ def draw_trms_pyramid() -> None: facecolor=BG, ) plt.close(fig) - print(" ✓ robot_trms_pyramid.png") + _logger.info("Generated robot_trms_pyramid.png") # ============================================================ @@ -411,31 +459,24 @@ def draw_vendor_comparison() -> None: facecolor=BG, ) plt.close(fig) - print(" ✓ robot_vendor_comparison.png") + _logger.info("Generated 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] +def _draw_ptp_subplot(ax: Axes) -> None: + """Draw the PTP (Point-to-Point) subplot.""" 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) + ax.set_title( + "PTP (Point-to-Point)\nMoveJ / PTP", + fontsize=8, + fontweight="bold", + ) + ax.grid(visible=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) @@ -476,13 +517,18 @@ def draw_movement_types() -> None: ax.set_ylabel("") ax.tick_params(labelsize=6) - # --- LIN (Linear) --- - ax = axes[1] + +def _draw_lin_subplot(ax: Axes) -> None: + """Draw the LIN (Linear) subplot.""" 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) + ax.set_title( + "LIN (Linear)\nMoveL / LIN", + fontsize=8, + fontweight="bold", + ) + ax.grid(visible=True, alpha=0.3) start = (0.5, 1.0) end = (3.5, 3.5) @@ -519,22 +565,27 @@ def draw_movement_types() -> None: ) ax.tick_params(labelsize=6) - # --- CIRC (Circular) --- - ax = axes[2] + +def _draw_circ_subplot(ax: Axes) -> None: + """Draw the CIRC (Circular) subplot.""" 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) + ax.set_title( + "CIRC (Circular)\nMoveC / CIRC", + fontsize=8, + fontweight="bold", + ) + ax.grid(visible=True, alpha=0.3) # Arc through 3 points center = (2.0, 1.5) - r = 2.0 + radius = 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) + x_circ = center[0] + radius * np.cos(theta) + y_circ = center[1] + radius * np.sin(theta) ax.plot(x_circ, y_circ, "k-", lw=2) ax.annotate( @@ -550,7 +601,11 @@ def draw_movement_types() -> None: 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" + 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) @@ -568,6 +623,21 @@ def draw_movement_types() -> None: ) ax.tick_params(labelsize=6) + +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, + ) + + _draw_ptp_subplot(axes[0]) + _draw_lin_subplot(axes[1]) + _draw_circ_subplot(axes[2]) + fig.tight_layout() fig.savefig( str(Path(OUTPUT_DIR) / "robot_movement_types.png"), @@ -576,7 +646,7 @@ def draw_movement_types() -> None: facecolor=BG, ) plt.close(fig) - print(" ✓ robot_movement_types.png") + _logger.info("Generated robot_movement_types.png") # ============================================================ @@ -664,7 +734,9 @@ def draw_online_offline() -> None: ax.text( 7.4, 0.6, - "✓ Bez zatrzymania produkcji\n✓ Wysoka precyzja, symulacja\n✗ Wymaga kalibracji", + "✓ Bez zatrzymania produkcji\n" + "✓ Wysoka precyzja, symulacja\n" + "✗ Wymaga kalibracji", ha="center", va="center", fontsize=6.5, @@ -679,7 +751,7 @@ def draw_online_offline() -> None: facecolor=BG, ) plt.close(fig) - print(" ✓ robot_online_offline.png") + _logger.info("Generated robot_online_offline.png") # ============================================================ @@ -714,7 +786,7 @@ def draw_ros_architecture() -> None: # Topics (arrows with labels) topics = [ - # (from_x, from_y, to_x, to_y, label) + # Fields: 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"), @@ -780,7 +852,7 @@ def draw_ros_architecture() -> None: facecolor=BG, ) plt.close(fig) - print(" ✓ robot_ros_architecture.png") + _logger.info("Generated robot_ros_architecture.png") # ============================================================ @@ -896,7 +968,9 @@ def draw_rapid_structure() -> None: ), ( 4.5, - "v500 = prędkość 500 mm/s\nz10 = strefa zbliżenia 10mm\nfine = dokładne dojście", + "v500 = prędkość 500 mm/s\n" + "z10 = strefa zbliżenia 10mm\n" + "fine = dokładne dojście", ), (2.5, "SetDO = Digital Output\nSterowanie I/O\n(chwytak, zawory)"), ] @@ -925,18 +999,19 @@ def draw_rapid_structure() -> None: facecolor=BG, ) plt.close(fig) - print(" ✓ robot_rapid_example.png") + _logger.info("Generated robot_rapid_example.png") # ============================================================ # Main # ============================================================ if __name__ == "__main__": - print("Generating PYTANIE 16 diagrams...") + logging.basicConfig(level=logging.INFO) + _logger.info("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) + _logger.info("Done! All diagrams saved to %s", OUTPUT_DIR) diff --git a/python_pkg/praca_magisterska_video/generate_images/generate_shortest_path_diagrams.py b/python_pkg/praca_magisterska_video/generate_images/generate_shortest_path_diagrams.py index 2925b5a..a352028 100755 --- a/python_pkg/praca_magisterska_video/generate_images/generate_shortest_path_diagrams.py +++ b/python_pkg/praca_magisterska_video/generate_images/generate_shortest_path_diagrams.py @@ -1,14 +1,19 @@ #!/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. + 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. """ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + import matplotlib as mpl mpl.use("Agg") @@ -17,47 +22,65 @@ from pathlib import Path import matplotlib.pyplot as plt import numpy as np +if TYPE_CHECKING: + from matplotlib.axes import Axes + +_logger = logging.getLogger(__name__) + 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)] +NODE_POS: dict[str, tuple[float, float]] = { + "A": (1, 2), + "B": (3.5, 3.2), + "C": (1, 0), + "D": (3.5, 0.8), +} +EDGES: list[tuple[str, str, int]] = [ + ("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, + ax: Axes, + name: str, + pos: tuple[float, float], + *, + color: str = "white", + current: bool = False, + visited: bool = False, + dist_label: str | None = None, + fontsize: int = 12, ) -> None: - """Draw a graph node (circle with label).""" + """Draw a graph node (circle with label). + + Args: + ax: Matplotlib axes to draw on. + name: Node label text. + pos: (x, y) position of the node center. + color: Fill color when not current/visited. + current: Whether this node is being processed. + visited: Whether this node has been visited. + dist_label: Optional distance label below node. + fontsize: Font size for the node label. + """ x, y = pos - r = 0.35 + radius = 0.35 lw = 2.5 if current else 1.5 ec = "#D32F2F" if current else LN fc = LIGHT_GREEN if visited else color @@ -65,7 +88,13 @@ def draw_graph_node( fc = LIGHT_YELLOW circle = plt.Circle( - (x, y), r, fill=True, facecolor=fc, edgecolor=ec, linewidth=lw, zorder=5 + (x, y), + radius, + fill=True, + facecolor=fc, + edgecolor=ec, + linewidth=lw, + zorder=5, ) ax.add_patch(circle) ax.text( @@ -97,25 +126,52 @@ def draw_graph_node( ) -def draw_graph_edge(ax, pos1, pos2, weight, highlighted=False, relaxed=False) -> None: - """Draw an edge between two nodes with weight label.""" +def draw_graph_edge( + ax: Axes, + pos1: tuple[float, float], + pos2: tuple[float, float], + weight: int, + *, + highlighted: bool = False, + relaxed: bool = False, +) -> None: + """Draw an edge between two nodes with weight label. + + Args: + ax: Matplotlib axes to draw on. + pos1: Start node position. + pos2: End node position. + weight: Edge weight value. + highlighted: Whether edge is highlighted. + relaxed: Whether edge was just relaxed. + """ 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 + node_radius = 0.38 + sx = x1 + node_radius * dx / length + sy = y1 + node_radius * dy / length + ex = x2 - node_radius * dx / length + ey = y2 - node_radius * dy / length - color = "#D32F2F" if relaxed else ("#1565C0" if highlighted else GRAY3) + 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) + ax.plot( + [sx, ex], + [sy, ey], + color=color, + linewidth=lw, + linestyle="-", + zorder=2, + ) # Weight label mx = (x1 + x2) / 2 @@ -135,7 +191,9 @@ def draw_graph_edge(ax, pos1, pos2, weight, highlighted=False, relaxed=False) -> bbox={ "boxstyle": "round,pad=0.15", "facecolor": "white", - "edgecolor": GRAY3 if not highlighted else color, + "edgecolor": ( + GRAY3 if not highlighted else color + ), "alpha": 0.95, }, zorder=4, @@ -143,15 +201,26 @@ def draw_graph_edge(ax, pos1, pos2, weight, highlighted=False, relaxed=False) -> def draw_full_graph( - ax, - title="", - dist=None, - current=None, - visited=None, - highlighted_edges=None, - relaxed_edges=None, + ax: Axes, + *, + title: str = "", + dist: dict[str, str] | None = None, + current: str | None = None, + visited: set[str] | None = None, + highlighted_edges: set[tuple[str, str]] | None = None, + relaxed_edges: set[tuple[str, str]] | None = None, ) -> None: - """Draw the complete graph with optional highlighting.""" + """Draw the complete graph with optional highlighting. + + Args: + ax: Matplotlib axes to draw on. + title: Subplot title. + dist: Distance labels per node. + current: Currently processed node name. + visited: Set of visited node names. + highlighted_edges: Edges to highlight. + relaxed_edges: Edges that were just relaxed. + """ if visited is None: visited = set() if highlighted_edges is None: @@ -170,17 +239,35 @@ def draw_full_graph( # 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) + 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) + for node_name, pos in NODE_POS.items(): + is_current = node_name == current + is_visited = node_name in visited + d_label = dist.get(node_name) draw_graph_node( - ax, name, pos, current=is_current, visited=is_visited, dist_label=d_label + ax, + node_name, + pos, + current=is_current, + visited=is_visited, + dist_label=d_label, ) @@ -188,7 +275,7 @@ def draw_full_graph( # 1. Graph structure diagram # ============================================================ def draw_graph_structure() -> None: - """The shared example graph used across all three algorithms.""" + """Draw the shared example graph used across all algorithms.""" _fig, ax = plt.subplots(1, 1, figsize=(5, 4)) ax.set_xlim(-0.5, 5.0) ax.set_ylim(-1.2, 4.5) @@ -207,8 +294,8 @@ def draw_graph_structure() -> None: 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) + for node_name, pos in NODE_POS.items(): + draw_graph_node(ax, node_name, pos) # Start arrow ax.annotate( @@ -241,14 +328,14 @@ def draw_graph_structure() -> None: facecolor=BG, ) plt.close() - print(" ✓ graph_example_structure.png") + _logger.info("graph_example_structure.png") # ============================================================ # 2. Dijkstra traversal # ============================================================ def draw_dijkstra_traversal() -> None: - """Step-by-step Dijkstra on the shared graph.""" + """Draw step-by-step Dijkstra on the shared graph.""" steps = [ { "title": "Krok 0: Inicjalizacja\nd = {A:0, B:∞, C:∞, D:∞}", @@ -259,7 +346,11 @@ def draw_dijkstra_traversal() -> None: "relaxed": set(), }, { - "title": "Krok 1: Przetwarzam A (d=0)\nRelaksacja: A→B: 0+2=2<∞ ✓ A→C: 0+4=4<∞ ✓", + "title": ( + "Krok 1: Przetwarzam A (d=0)\n" + "Relaksacja: A→B: 0+2=2<∞ ✓" + " A→C: 0+4=4<∞ ✓" + ), "dist": {"A": "0", "B": "2", "C": "4", "D": "∞"}, "current": "A", "visited": {"A"}, @@ -267,7 +358,11 @@ def draw_dijkstra_traversal() -> None: "relaxed": {("A", "B"), ("A", "C")}, }, { - "title": "Krok 2: Przetwarzam B (d=2) — minimum\nRelaksacja: B→D: 2+3=5<∞ ✓", + "title": ( + "Krok 2: Przetwarzam B (d=2)" + " — minimum\n" + "Relaksacja: B→D: 2+3=5<∞ ✓" + ), "dist": {"A": "0", "B": "2", "C": "4", "D": "5"}, "current": "B", "visited": {"A", "B"}, @@ -275,7 +370,11 @@ def draw_dijkstra_traversal() -> None: "relaxed": {("B", "D")}, }, { - "title": "Krok 3: Przetwarzam C (d=4)\nRelaksacja: C→D: 4+5=9 > 5 ✗ (nie poprawia)", + "title": ( + "Krok 3: Przetwarzam C (d=4)\n" + "Relaksacja: C→D: 4+5=9 > 5" + " ✗ (nie poprawia)" + ), "dist": {"A": "0", "B": "2", "C": "4", "D": "5"}, "current": "C", "visited": {"A", "B", "C"}, @@ -283,7 +382,11 @@ def draw_dijkstra_traversal() -> None: "relaxed": set(), }, { - "title": "Krok 4: WYNIK — wszystkie przetworzone\nd = {A:0, B:2, C:4, D:5}", + "title": ( + "Krok 4: WYNIK" + " — wszystkie przetworzone\n" + "d = {A:0, B:2, C:4, D:5}" + ), "dist": {"A": "0", "B": "2", "C": "4", "D": "5"}, "current": None, "visited": {"A", "B", "C", "D"}, @@ -294,7 +397,8 @@ def draw_dijkstra_traversal() -> None: 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)", + "Dijkstra — przejście grafu krok po kroku" + " (zachłannie: zawsze bierz min d)", fontsize=FS_TITLE, fontweight="bold", y=1.02, @@ -315,11 +419,17 @@ def draw_dijkstra_traversal() -> None: fig.text( 0.5, -0.04, - "[zolty] = aktualnie przetwarzany [zielony] = odwiedzony (zamkniety) " - "czerwona krawedz = relaksacja OK szara krawedz = nie poprawia", + "[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}, + bbox={ + "boxstyle": "round,pad=0.3", + "facecolor": GRAY4, + "edgecolor": GRAY3, + }, ) plt.tight_layout() @@ -330,18 +440,19 @@ def draw_dijkstra_traversal() -> None: facecolor=BG, ) plt.close() - print(" ✓ dijkstra_traversal.png") + _logger.info("dijkstra_traversal.png") # ============================================================ # 3. Bellman-Ford traversal # ============================================================ def draw_bellman_ford_traversal() -> None: - """Step-by-step Bellman-Ford on the shared graph.""" + """Draw 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)", + "(V-1 = 3 iteracje, w każdej relaksuj" + " WSZYSTKIE krawędzie)", fontsize=FS_TITLE, fontweight="bold", y=0.98, @@ -408,14 +519,23 @@ def draw_bellman_ford_traversal() -> None: ) # Negative cycle check note + neg_cycle_msg = ( + "Po 3 iteracjach: sprawdz raz jeszcze" + " — nic sie nie zmienia" + " → BRAK cyklu ujemnego → wynik poprawny" + ) fig.text( 0.5, 0.01, - "Po 3 iteracjach: sprawdź raz jeszcze — nic się nie zmienia → BRAK cyklu ujemnego → wynik poprawny", + neg_cycle_msg, ha="center", fontsize=FS, fontweight="bold", - bbox={"boxstyle": "round,pad=0.3", "facecolor": LIGHT_GREEN, "edgecolor": LN}, + bbox={ + "boxstyle": "round,pad=0.3", + "facecolor": LIGHT_GREEN, + "edgecolor": LN, + }, ) plt.tight_layout(rect=[0, 0.05, 1, 0.95]) @@ -426,21 +546,22 @@ def draw_bellman_ford_traversal() -> None: facecolor=BG, ) plt.close() - print(" ✓ bellman_ford_traversal.png") + _logger.info("bellman_ford_traversal.png") # ============================================================ # 4. A* traversal # ============================================================ def draw_astar_traversal() -> None: - """Step-by-step A* on the shared graph with heuristics.""" + """Draw 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", + "f(n) = g(n) + h(n), heurystyka h" + " = oszacowana odległość do D", fontsize=FS_TITLE, fontweight="bold", y=0.99, @@ -520,11 +641,11 @@ def draw_astar_traversal() -> None: ) # Add h values as small labels - for name, pos in NODE_POS.items(): + for node_name, pos in NODE_POS.items(): ax_g.text( pos[0] + 0.35, pos[1] + 0.35, - f"h={h_vals[name]}", + f"h={h_vals[node_name]}", ha="center", va="center", fontsize=5.5, @@ -558,8 +679,11 @@ def draw_astar_traversal() -> None: 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.", + "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", @@ -578,16 +702,17 @@ def draw_astar_traversal() -> None: facecolor=BG, ) plt.close() - print(" ✓ astar_traversal.png") + _logger.info("astar_traversal.png") # ============================================================ # Main # ============================================================ if __name__ == "__main__": - print("Generating shortest path diagrams for PYTANIE 2...") + logging.basicConfig(level=logging.INFO) + _logger.info("Generating shortest path diagrams...") draw_graph_structure() draw_dijkstra_traversal() draw_bellman_ford_traversal() draw_astar_traversal() - print(f"\nAll diagrams saved to {OUTPUT_DIR}/") + _logger.info("All diagrams saved to %s/", OUTPUT_DIR) diff --git a/python_pkg/praca_magisterska_video/generate_images/split_questions.py b/python_pkg/praca_magisterska_video/generate_images/split_questions.py index c84dc02..96438c3 100755 --- a/python_pkg/praca_magisterska_video/generate_images/split_questions.py +++ b/python_pkg/praca_magisterska_video/generate_images/split_questions.py @@ -5,6 +5,9 @@ Each file: pytanie_NN.md (or pytanie_NN_MM.md for dual-numbered like 13/27). Placed in pytania/questions/ folder. """ +from __future__ import annotations + +import logging from pathlib import Path import re @@ -13,6 +16,9 @@ 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) +logging.basicConfig(level=logging.INFO) +_logger = logging.getLogger(__name__) + with Path(SOURCE).open(encoding="utf-8") as f: lines = f.readlines() @@ -25,7 +31,7 @@ for i, line in enumerate(lines): title = m.group(3).strip() question_starts.append((i, raw_num, title)) -print(f"Found {len(question_starts)} questions") +_logger.info("Found %d questions", len(question_starts)) for idx, (start_line, raw_num, title) in enumerate(question_starts): # End = next question start or EOF @@ -54,6 +60,12 @@ for idx, (start_line, raw_num, title) in enumerate(question_starts): f.writelines(content_lines) line_count = len(content_lines) - print(f" {filename:30s} ({line_count:4d} lines) PYTANIE {raw_num}: {title}") + _logger.info( + " %-30s (%4d lines) PYTANIE %s: %s", + filename, + line_count, + raw_num, + title, + ) -print(f"\nAll files written to: {OUT_DIR}") +_logger.info("All files written to: %s", OUT_DIR)