refactor(praca/generate_images): fix ruff violations in diagram generators batch 1

This commit is contained in:
Krzysztof kuhy Rudnicki 2026-03-14 14:29:54 +01:00
parent 03409b6839
commit 7c47befe04
7 changed files with 1201 additions and 679 deletions

View File

@ -1,13 +1,18 @@
#!/usr/bin/env python3 #!/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 1: Graph with negative edge, Dijkstra WRONG vs Bellman-Ford CORRECT
Part 2: Negative cycle detection (add CB(-3)) Part 2: Negative cycle detection (add C->B(-3))
A4-compatible, monochrome-friendly, 300 DPI. A4-compatible, monochrome-friendly, 300 DPI.
""" """
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import matplotlib as mpl import matplotlib as mpl
mpl.use("Agg") mpl.use("Agg")
@ -16,6 +21,11 @@ from pathlib import Path
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
if TYPE_CHECKING:
from matplotlib.axes import Axes
_logger = logging.getLogger(__name__)
DPI = 300 DPI = 300
BG = "white" BG = "white"
LN = "black" LN = "black"
@ -34,34 +44,53 @@ LIGHT_GREEN = "#D5E8D4"
LIGHT_RED = "#F8D7DA" LIGHT_RED = "#F8D7DA"
LIGHT_YELLOW = "#FFF9C4" LIGHT_YELLOW = "#FFF9C4"
# --- Graph layout for negative-weight example --- # Graph layout for negative-weight example:
# S→A(2), A→C(3), S→B(5), B→A(-4) # 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_POS: dict[str, tuple[float, float]] = {
NEG_EDGES = [("S", "A", 2), ("A", "C", 3), ("S", "B", 5), ("B", "A", -4)] "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( def draw_node(
ax, ax: Axes,
name, name: str,
pos, pos: tuple[float, float],
color="white", *,
current=False, color: str = "white",
visited=False, current: bool = False,
dist_label=None, visited: bool = False,
fontsize=12, dist_label: str | None = None,
error=False, fontsize: int = 12,
error: bool = False,
) -> None: ) -> None:
"""Draw node.""" """Draw a graph node with optional distance label."""
x, y = pos x, y = pos
r = 0.35 r = 0.35
lw = 2.5 if current else 1.5 lw = 2.5 if current else 1.5
ec = "#D32F2F" if current else ("#D32F2F" if error else LN) 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: if error:
fc = LIGHT_RED fc = LIGHT_RED
circle = plt.Circle( 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.add_patch(circle)
ax.text( 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( def draw_edge(
ax, ax: Axes,
pos1, pos1: tuple[float, float],
pos2, pos2: tuple[float, float],
weight, weight: int,
highlighted=False, *,
relaxed=False, highlighted: bool = False,
negative=False, relaxed: bool = False,
cycle_edge=False, negative: bool = False,
offset=0.0, cycle_edge: bool = False,
offset: float = 0.0,
) -> None: ) -> None:
"""Draw edge.""" """Draw a directed edge between two nodes with a weight label."""
x1, y1 = pos1 x1, y1 = pos1
x2, y2 = pos2 x2, y2 = pos2
@ -127,22 +174,12 @@ def draw_edge(
ex += perp_x ex += perp_x
ey += perp_y ey += perp_y
if cycle_edge: color, lw, ls = _choose_edge_style(
color = "#D32F2F" negative=negative,
lw = 2.5 relaxed=relaxed,
ls = "--" highlighted=highlighted,
elif negative or relaxed: cycle_edge=cycle_edge,
color = "#D32F2F" )
lw = 2.5
ls = "-"
elif highlighted:
color = "#1565C0"
lw = 2.0
ls = "-"
else:
color = GRAY3
lw = 1.5
ls = "-"
# Arrow # Arrow
ax.annotate( ax.annotate(
@ -191,18 +228,19 @@ def draw_edge(
def draw_neg_graph( def draw_neg_graph(
ax, ax: Axes,
edges, edges: list[tuple[str, str, int]],
title="", *,
dist=None, title: str = "",
current=None, dist: dict[str, str] | None = None,
visited=None, current: str | None = None,
relaxed_edges=None, visited: set[str] | None = None,
error_nodes=None, relaxed_edges: set[tuple[str, str]] | None = None,
extra_edges=None, error_nodes: set[str] | None = None,
node_positions=None, extra_edges: list[tuple[str, str, int]] | None = None,
node_positions: dict[str, tuple[float, float]] | None = None,
) -> None: ) -> None:
"""Draw neg graph.""" """Draw the negative-weight graph with annotations."""
if visited is None: if visited is None:
visited = set() visited = set()
if relaxed_edges is None: if relaxed_edges is None:
@ -228,9 +266,7 @@ def draw_neg_graph(
for u, v, w in all_edges: for u, v, w in all_edges:
rl = (u, v) in relaxed_edges rl = (u, v) in relaxed_edges
neg = w < 0 neg = w < 0
cycle = extra_edges and (u, v, w) in extra_edges cycle = bool(extra_edges and (u, v, w) in extra_edges)
# If B→A and A→B both exist, offset them
off = 0.0
draw_edge( draw_edge(
ax, ax,
node_positions[u], node_positions[u],
@ -239,7 +275,7 @@ def draw_neg_graph(
relaxed=rl, relaxed=rl,
negative=neg, negative=neg,
cycle_edge=cycle, cycle_edge=cycle,
offset=off, offset=0.0,
) )
for name, pos in node_positions.items(): 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: def generate_bf_negative_weights() -> None:
"""Two-row figure. """Generate two-row figure.
Row 1: Graph structure + Dijkstra WRONG + Bellman-Ford CORRECT Row 1: Graph structure + Dijkstra WRONG + Bellman-Ford CORRECT
Row 2: B-F iterations 1-3 step by step. Row 2: B-F iterations 1-3 step by step.
""" """
fig = plt.figure(figsize=(14, 10)) fig = plt.figure(figsize=(14, 10))
fig.suptitle( fig.suptitle(
"Bellman-Ford — ujemne wagi vs Dijkstra\n" "Bellman-Ford \u2014 ujemne wagi vs Dijkstra\n"
"Graf: S→A(2), A→C(3), S→B(5), B→A(-4). Start = S", "Graf: S\u2192A(2), A\u2192C(3),"
" S\u2192B(5), B\u2192A(-4). Start = S",
fontsize=FS_TITLE + 1, fontsize=FS_TITLE + 1,
fontweight="bold", fontweight="bold",
y=0.99, y=0.99,
) )
# ---- Row 1: Graph + Dijkstra wrong + BF correct ---- # Row 1: Graph + Dijkstra wrong + BF correct
# Panel 1: The graph structure # Panel 1: The graph structure
ax1 = fig.add_subplot(2, 3, 1) ax1 = fig.add_subplot(2, 3, 1)
draw_neg_graph( draw_neg_graph(
ax1, ax1,
NEG_EDGES, 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": "?"}, dist={"S": "0", "A": "?", "B": "?", "C": "?"},
) )
# START label
ax1.annotate( ax1.annotate(
"START", "START",
xy=(NEG_POS["S"][0] - 0.35, NEG_POS["S"][1]), xy=(NEG_POS["S"][0] - 0.35, NEG_POS["S"][1]),
@ -291,7 +357,11 @@ def generate_bf_negative_weights() -> None:
fontsize=FS, fontsize=FS,
fontweight="bold", fontweight="bold",
color="#D32F2F", color="#D32F2F",
arrowprops={"arrowstyle": "->", "color": "#D32F2F", "lw": 2}, arrowprops={
"arrowstyle": "->",
"color": "#D32F2F",
"lw": 2,
},
va="center", va="center",
) )
@ -300,41 +370,29 @@ def generate_bf_negative_weights() -> None:
draw_neg_graph( draw_neg_graph(
ax2, ax2,
NEG_EDGES, 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"}, dist={"S": "0", "A": "2", "B": "5", "C": "5"},
visited={"S", "A", "B", "C"}, visited={"S", "A", "B", "C"},
error_nodes={"A", "C"}, error_nodes={"A", "C"},
) )
# Add "WRONG" annotations _add_annotation_box(
ax2.text( ax2,
NEG_POS["A"][0] + 0.6, NEG_POS["A"][0] + 0.6,
NEG_POS["A"][1] + 0.3, NEG_POS["A"][1] + 0.3,
"✗ powinno 1", "✗ powinno 1",
fontsize=FS_SMALL,
color="#D32F2F", color="#D32F2F",
fontweight="bold", bg_color=LIGHT_RED,
bbox={
"boxstyle": "round,pad=0.1",
"facecolor": LIGHT_RED,
"edgecolor": "#D32F2F",
"alpha": 0.9,
"lw": 0.5,
},
) )
ax2.text( _add_annotation_box(
ax2,
NEG_POS["C"][0] + 0.05, NEG_POS["C"][0] + 0.05,
NEG_POS["C"][1] + 0.55, NEG_POS["C"][1] + 0.55,
"✗ powinno 4", "✗ powinno 4",
fontsize=FS_SMALL,
color="#D32F2F", color="#D32F2F",
fontweight="bold", bg_color=LIGHT_RED,
bbox={
"boxstyle": "round,pad=0.1",
"facecolor": LIGHT_RED,
"edgecolor": "#D32F2F",
"alpha": 0.9,
"lw": 0.5,
},
) )
# Panel 3: Bellman-Ford — CORRECT # Panel 3: Bellman-Ford — CORRECT
@ -342,48 +400,45 @@ def generate_bf_negative_weights() -> None:
draw_neg_graph( draw_neg_graph(
ax3, ax3,
NEG_EDGES, 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"}, dist={"S": "0", "A": "1", "B": "5", "C": "4"},
visited={"S", "A", "B", "C"}, visited={"S", "A", "B", "C"},
relaxed_edges={("B", "A")}, relaxed_edges={("B", "A")},
) )
ax3.text( _add_annotation_box(
ax3,
NEG_POS["A"][0] + 0.6, NEG_POS["A"][0] + 0.6,
NEG_POS["A"][1] + 0.3, NEG_POS["A"][1] + 0.3,
"✓ poprawne!", "✓ poprawne!",
fontsize=FS_SMALL,
color="#006400", color="#006400",
fontweight="bold", bg_color=LIGHT_GREEN,
bbox={
"boxstyle": "round,pad=0.1",
"facecolor": LIGHT_GREEN,
"edgecolor": "#006400",
"alpha": 0.9,
"lw": 0.5,
},
) )
ax3.text( _add_annotation_box(
ax3,
NEG_POS["C"][0] + 0.05, NEG_POS["C"][0] + 0.05,
NEG_POS["C"][1] + 0.55, NEG_POS["C"][1] + 0.55,
"✓ poprawne!", "✓ poprawne!",
fontsize=FS_SMALL,
color="#006400", color="#006400",
fontweight="bold", bg_color=LIGHT_GREEN,
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 ---- # Row 2: B-F iterations step by step
iterations = [ iterations = [
{ {
"title": "B-F Iteracja 1\nRelaksuj WSZYSTKIE krawędzie", "title": (
"dist": {"S": "0", "A": "1", "B": "5", "C": "5"}, "B-F Iteracja 1\n"
"relaxed": {("S", "A"), ("A", "C"), ("S", "B"), ("B", "A")}, "Relaksuj WSZYSTKIE krawędzie"
),
"dist": {
"S": "0", "A": "1", "B": "5", "C": "5",
},
"relaxed": {
("S", "A"), ("A", "C"),
("S", "B"), ("B", "A"),
},
"detail": ( "detail": (
"S→A: 0+2=2<∞ → A=2\n" "S→A: 0+2=2<∞ → A=2\n"
"A→C: 2+3=5<∞ → C=5\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", "title": (
"dist": {"S": "0", "A": "1", "B": "5", "C": "4"}, "B-F Iteracja 2\n"
"Propagacja poprawionego A"
),
"dist": {
"S": "0", "A": "1", "B": "5", "C": "4",
},
"relaxed": {("A", "C")}, "relaxed": {("A", "C")},
"detail": ( "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!", "title": (
"dist": {"S": "0", "A": "1", "B": "5", "C": "4"}, "B-F Iteracja 3\n"
"Brak zmian → stabilne!"
),
"dist": {
"S": "0", "A": "1", "B": "5", "C": "4",
},
"relaxed": set(), "relaxed": set(),
"detail": ( "detail": (
"Wszystkie krawędzie:\n" "Wszystkie krawędzie:\n"
@ -422,7 +490,6 @@ def generate_bf_negative_weights() -> None:
visited={"S", "A", "B", "C"}, visited={"S", "A", "B", "C"},
relaxed_edges=it["relaxed"], relaxed_edges=it["relaxed"],
) )
# Detail text below graph
ax.text( ax.text(
3.2, 3.2,
-0.5, -0.5,
@ -431,19 +498,31 @@ def generate_bf_negative_weights() -> None:
va="top", va="top",
fontsize=FS_SMALL, fontsize=FS_SMALL,
family="monospace", family="monospace",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3}, bbox={
"boxstyle": "round,pad=0.3",
"facecolor": GRAY4,
"edgecolor": GRAY3,
},
) )
# Bottom note # Bottom note
fig.text( fig.text(
0.5, 0.5,
0.01, 0.01,
"Dijkstra zamyka wierzchołki na stałe (zachłanność) → ujemna waga B→A(-4) nie może poprawić zamkniętego A.\n" "Dijkstra zamyka wierzchołki na stałe"
"Bellman-Ford relaksuje WSZYSTKIE krawędzie w każdej iteracji → ujemne wagi propagują się poprawnie.", " (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", ha="center",
fontsize=FS, fontsize=FS,
fontweight="bold", 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]) plt.tight_layout(rect=[0, 0.05, 1, 0.95])
@ -454,19 +533,20 @@ def generate_bf_negative_weights() -> None:
facecolor=BG, facecolor=BG,
) )
plt.close() plt.close()
print(" ✓ bellman_ford_negative_weights.png") _logger.info(" ✓ bellman_ford_negative_weights.png")
def generate_bf_negative_cycle() -> None: def generate_bf_negative_cycle() -> None:
"""Figure showing negative cycle detection. """Generate figure showing negative cycle detection.
Graph: SA(2), AC(3), SB(5), BA(-4), CB(-3) [added edge] Graph: S->A(2), A->C(3), S->B(5), B->A(-4), C->B(-3)
Cycle: BACB = -4+3+(-3) = -4 < 0. Cycle: B->A->C->B = -4+3+(-3) = -4 < 0.
""" """
fig = plt.figure(figsize=(14, 5.5)) fig = plt.figure(figsize=(14, 5.5))
fig.suptitle( fig.suptitle(
"Bellman-Ford — wykrywanie cyklu ujemnego\n" "Bellman-Ford \u2014 wykrywanie cyklu ujemnego\n"
"Dodano krawędź C→B(-3). Cykl: B→A→C→B = -4+3+(-3) = -4 < 0", "Dodano krawędź C→B(-3)."
" Cykl: B→A→C→B = -4+3+(-3) = -4 < 0",
fontsize=FS_TITLE + 1, fontsize=FS_TITLE + 1,
fontweight="bold", fontweight="bold",
y=0.99, y=0.99,
@ -477,11 +557,13 @@ def generate_bf_negative_cycle() -> None:
draw_neg_graph( draw_neg_graph(
ax1, ax1,
NEG_EDGES, 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": "?"}, dist={"S": "0", "A": "?", "B": "?", "C": "?"},
extra_edges=[("C", "B", -3)], extra_edges=[("C", "B", -3)],
) )
# Mark cycle
ax1.annotate( ax1.annotate(
"CYKL\n-4+3+(-3)=-4<0", "CYKL\n-4+3+(-3)=-4<0",
xy=(3.3, 2.0), xy=(3.3, 2.0),
@ -503,7 +585,10 @@ def generate_bf_negative_cycle() -> None:
draw_neg_graph( draw_neg_graph(
ax2, ax2,
NEG_EDGES, 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"}, dist={"S": "0", "A": "-7", "B": "-4", "C": "-4"},
visited={"S", "A", "B", "C"}, visited={"S", "A", "B", "C"},
error_nodes={"A", "B", "C"}, error_nodes={"A", "B", "C"},
@ -512,7 +597,9 @@ def generate_bf_negative_cycle() -> None:
ax2.text( ax2.text(
3.2, 3.2,
-0.4, -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", ha="center",
va="top", va="top",
fontsize=FS_SMALL, fontsize=FS_SMALL,
@ -562,7 +649,8 @@ def generate_bf_negative_cycle() -> None:
}, },
) )
ax3.set_title( 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, fontsize=FS,
fontweight="bold", fontweight="bold",
pad=5, pad=5,
@ -572,12 +660,19 @@ def generate_bf_negative_cycle() -> None:
fig.text( fig.text(
0.5, 0.5,
0.01, 0.01,
"Bez cyklu ujemnego: po V-1 iteracjach dist jest stabilne. " "Bez cyklu ujemnego: po V-1 iteracjach"
"Z cyklem ujemnym: dist maleje w nieskończoność → V-ta iteracja to wykrywa.", " dist jest stabilne. "
"Z cyklem ujemnym: dist maleje"
" w nieskończoność"
" → V-ta iteracja to wykrywa.",
ha="center", ha="center",
fontsize=FS, fontsize=FS,
fontweight="bold", 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]) plt.tight_layout(rect=[0, 0.06, 1, 0.94])
@ -588,11 +683,14 @@ def generate_bf_negative_cycle() -> None:
facecolor=BG, facecolor=BG,
) )
plt.close() plt.close()
print(" ✓ bellman_ford_negative_cycle.png") _logger.info(" ✓ bellman_ford_negative_cycle.png")
if __name__ == "__main__": 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_weights()
generate_bf_negative_cycle() generate_bf_negative_cycle()
print(f"\nAll diagrams saved to {OUTPUT_DIR}/") _logger.info("All diagrams saved to %s/", OUTPUT_DIR)

View File

@ -582,7 +582,7 @@ def draw_3nf() -> None:
"""Draw 3nf.""" """Draw 3nf."""
fig, ax = create_figure(11.69, 6.5) fig, ax = create_figure(11.69, 6.5)
# Studenci (fixed) # Student table after removing transitive dependency
h1 = ["StID*", "Imie", "WydzialID"] h1 = ["StID*", "Imie", "WydzialID"]
r1 = [["1", "Anna", "W4"], ["2", "Jan", "W4"], ["3", "Ewa", "W2"]] r1 = [["1", "Anna", "W4"], ["2", "Jan", "W4"], ["3", "Ewa", "W2"]]
cw1 = [0.55, 0.55, 0.85] cw1 = [0.55, 0.55, 0.85]
@ -633,7 +633,8 @@ def draw_3nf() -> None:
ax, ax,
0.3, 0.3,
2.95, 2.95,
" StID -> WydzialID -> NazwaWydzialu rozbito: NazwaWydzialu w osobnej tabeli.", " StID -> WydzialID -> NazwaWydzialu"
" rozbito: NazwaWydzialu w osobnej tabeli.",
fontsize=8, fontsize=8,
color="#333333", color="#333333",
) )
@ -665,7 +666,8 @@ def draw_3nf() -> None:
ax, ax,
0.3, 0.3,
1.5, 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, fontsize=9,
color="#333333", color="#333333",
) )
@ -677,7 +679,7 @@ def draw_3nf() -> None:
pad_inches=0.2, pad_inches=0.2,
) )
plt.close(fig) 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 ax, 7.2, 6.8, "ProwadzacyKurs (kl: Prow.)", h4, r4, cw4, title_fontsize=9
) )
# StudentProwadzacy (NEW) # New student-advisor junction table
h5 = ["StID*", "Prowadzacy*"] h5 = ["StID*", "Prowadzacy*"]
r5 = [["1", "Kowalski"], ["1", "Nowak"], ["2", "Kowalski"], ["3", "Wisniewski"]] r5 = [["1", "Kowalski"], ["1", "Nowak"], ["2", "Kowalski"], ["3", "Wisniewski"]]
cw5 = [0.55, 1.05] cw5 = [0.55, 1.05]
@ -733,7 +735,8 @@ def draw_bcnf() -> None:
ax, ax,
0.3, 0.3,
2.55, 2.55,
" ProwadzacyKurs(Prowadzacy, KursID) — FD: Prowadzacy -> KursID, klucz: Prowadzacy", " ProwadzacyKurs(Prowadzacy, KursID)"
" — FD: Prowadzacy -> KursID, klucz: Prowadzacy",
fontsize=8, fontsize=8,
color="#333333", color="#333333",
) )
@ -756,7 +759,8 @@ def draw_bcnf() -> None:
ax, ax,
0.3, 0.3,
1.45, 1.45,
"Rekonstrukcja: StudentProw. JOIN ProwadzacyKurs ON Prowadzacy -> odtworzenie Zapisy.", "Rekonstrukcja: StudentProw. JOIN ProwadzacyKurs"
" ON Prowadzacy -> odtworzenie Zapisy.",
fontsize=8, fontsize=8,
color="#333333", color="#333333",
) )
@ -768,7 +772,7 @@ def draw_bcnf() -> None:
pad_inches=0.2, pad_inches=0.2,
) )
plt.close(fig) 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, pad_inches=0.2,
) )
plt.close(fig) 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, ax,
0.3, 0.3,
2.0, 2.0,
"Weryfikacja: Alfa dostarcza Nakretke? Alfa -> Wiezowiec? Nakretka -> Wiezowiec?", "Weryfikacja: Alfa dostarcza Nakretke?"
" Alfa -> Wiezowiec? Nakretka -> Wiezowiec?",
fontsize=8, fontsize=8,
) )
add_label( add_label(
ax, ax,
0.3, 0.3,
1.65, 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, fontsize=8,
color="#333333", color="#333333",
) )
@ -1035,7 +1041,8 @@ def draw_5nf() -> None:
ax, ax,
0.3, 0.3,
0.9, 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, fontsize=8,
color="black", color="black",
) )
@ -1043,7 +1050,8 @@ def draw_5nf() -> None:
ax, ax,
0.3, 0.3,
0.5, 0.5,
" Dekompozycja 5NF jest poprawna TYLKO jesli regula cykliczna rzeczywiscie zachodzi!", " Dekompozycja 5NF jest poprawna TYLKO"
" jesli regula cykliczna rzeczywiscie zachodzi!",
fontsize=8, fontsize=8,
color="black", color="black",
) )
@ -1055,7 +1063,7 @@ def draw_5nf() -> None:
pad_inches=0.2, pad_inches=0.2,
) )
plt.close(fig) 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}, arrowprops={"arrowstyle": "<-", "color": "black", "lw": 1.5},
) )
# Bottom: mnemonic # Mnemonic quote at the bottom
ax.text( ax.text(
5.85, 5.85,
2.2, 2.2,
@ -1178,7 +1186,8 @@ def draw_summary_flow() -> None:
ax.text( ax.text(
5.85, 5.85,
0.8, 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, fontsize=8,
ha="center", ha="center",
va="center", va="center",
@ -1193,14 +1202,15 @@ def draw_summary_flow() -> None:
pad_inches=0.2, pad_inches=0.2,
) )
plt.close(fig) plt.close(fig)
print("Generated: nf_summary_flow.png") logger.info("Generated: nf_summary_flow.png")
# ============================================================ # ============================================================
# Main # Main
# ============================================================ # ============================================================
if __name__ == "__main__": if __name__ == "__main__":
print("Generating normalization diagrams...") logging.basicConfig(level=logging.INFO)
logger.info("Generating normalization diagrams...")
draw_0nf() draw_0nf()
draw_1nf() draw_1nf()
draw_2nf() draw_2nf()
@ -1209,4 +1219,4 @@ if __name__ == "__main__":
draw_4nf() draw_4nf()
draw_5nf() draw_5nf()
draw_summary_flow() draw_summary_flow()
print("\nDone! All diagrams saved to:", OUTPUT_DIR) logger.info("Done! All diagrams saved to %s", OUTPUT_DIR)

View File

@ -8,6 +8,11 @@
All: A4-compatible, B&W, 300 DPI, laser-printer-friendly. 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 import matplotlib as mpl
mpl.use("Agg") mpl.use("Agg")
@ -18,6 +23,11 @@ from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
if TYPE_CHECKING:
from matplotlib.axes import Axes
_logger = logging.getLogger(__name__)
DPI = 300 DPI = 300
BG = "white" BG = "white"
LN = "black" LN = "black"
@ -33,29 +43,40 @@ GRAY3 = "#B8B8B8"
GRAY4 = "#F5F5F5" GRAY4 = "#F5F5F5"
GRAY5 = "#C0C0C0" GRAY5 = "#C0C0C0"
_BAND_HEIGHTS = [0.7, 1.3, 1.4, 1.5, 1.5]
def draw_box( def draw_box(
ax, ax: Axes,
x, x: float,
y, y: float,
w, w: float,
h, h: float,
text, text: str,
fill="white", *,
lw=1.2, fill: str = "white",
fontsize=FS, lw: float = 1.2,
fontweight="normal", fontsize: float = FS,
ha="center", fontweight: str = "normal",
va="center", ha: str = "center",
rounded=True, va: str = "center",
rounded: bool = True,
) -> None: ) -> None:
"""Draw box.""" """Draw a labeled box on the axes."""
if rounded: if rounded:
rect = FancyBboxPatch( 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: 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.add_patch(rect)
ax.text( ax.text(
x + w / 2, 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: def draw_arrow(
"""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( ax.annotate(
"", "",
xy=(x2, y2), 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) # 1. Pattern Template Structure (NaPSiRoKo mnemonic)
# ============================================================ # ============================================================
def generate_pattern_template() -> None: def generate_pattern_template() -> None:
"""Generate pattern template.""" """Generate pattern template diagram with NaPSiRoKo mnemonic."""
fig, ax = plt.subplots(figsize=(8.27, 6)) fig, ax = plt.subplots(figsize=(8.27, 6))
ax.set_xlim(0, 10) ax.set_xlim(0, 10)
ax.set_ylim(0, 8) ax.set_ylim(0, 8)
@ -133,11 +164,17 @@ def generate_pattern_template() -> None:
( (
"Si", "Si",
"SIŁY (forces)", "SIŁY (forces)",
"Konkurencyjne wymagania do pogodzenia\n(np. testowalność vs wydajność)", "Konkurencyjne wymagania do pogodzenia\n"
"(np. testowalność vs wydajność)",
GRAY1, GRAY1,
), ),
("Ro", "ROZWIĄZANIE", "Struktura, diagram, zachowanie", "white"), ("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 band_x = card_x + 0.3
@ -202,7 +239,14 @@ def generate_pattern_template() -> None:
# Arrow connecting fields # Arrow connecting fields
if i < len(fields) - 1: 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 # Extra fields note at bottom
ax.text( ax.text(
@ -232,14 +276,14 @@ def generate_pattern_template() -> None:
out = str(Path(OUTPUT_DIR) / "q14_pattern_template.png") out = str(Path(OUTPUT_DIR) / "q14_pattern_template.png")
fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG) fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG)
plt.close(fig) plt.close(fig)
print(f" Saved: {out}") _logger.info(" Saved: %s", out)
# ============================================================ # ============================================================
# 2. Catalog Classification Map # 2. Catalog Classification Map
# ============================================================ # ============================================================
def generate_catalog_map() -> None: def generate_catalog_map() -> None:
"""Generate catalog map.""" """Generate catalog classification map diagram."""
fig, ax = plt.subplots(figsize=(8.27, 7)) fig, ax = plt.subplots(figsize=(8.27, 7))
ax.set_xlim(0, 12) ax.set_xlim(0, 12)
ax.set_ylim(0, 9) ax.set_ylim(0, 9)
@ -247,13 +291,15 @@ def generate_catalog_map() -> None:
ax.axis("off") ax.axis("off")
fig.patch.set_facecolor(BG) fig.patch.set_facecolor(BG)
ax.set_title( 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, fontsize=FS_TITLE,
fontweight="bold", fontweight="bold",
pad=15, pad=15,
) )
# Y-axis: Scale (architectural → design → idiom) # Y-axis: Scale (architectural -> design -> idiom)
ax.text( ax.text(
0.3, 0.3,
7.8, 7.8,
@ -286,7 +332,9 @@ def generate_catalog_map() -> None:
va="center", va="center",
fontstyle="italic", 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 # X-axis: Domain
ax.text( ax.text(
@ -305,16 +353,16 @@ def generate_catalog_map() -> None:
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN}, arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
) )
# Catalog boxes positioned by scale x domain # Catalog boxes positioned by scale and domain
catalogs = [ catalogs = [
# (x, y, w, h, name, subtitle, fill, mnemonic_letter)
( (
2.5, 2.5,
6.2, 6.2,
2.5, 2.5,
1.4, 1.4,
"POSA", "POSA",
"1996 • Buschmann\nLayers, Broker,\nPipes & Filters, MVC", "1996 • Buschmann\nLayers, Broker,\n"
"Pipes & Filters, MVC",
GRAY1, GRAY1,
"P", "P",
), ),
@ -324,7 +372,8 @@ def generate_catalog_map() -> None:
2.5, 2.5,
1.4, 1.4,
"GoF", "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, GRAY2,
"G", "G",
), ),
@ -334,7 +383,8 @@ def generate_catalog_map() -> None:
2.5, 2.5,
1.4, 1.4,
"EIP", "EIP",
"2003 • Hohpe & Woolf\nMessage Channel,\nRouter, Aggregator", "2003 • Hohpe & Woolf\nMessage Channel,\n"
"Router, Aggregator",
GRAY1, GRAY1,
"E", "E",
), ),
@ -344,7 +394,8 @@ def generate_catalog_map() -> None:
2.5, 2.5,
1.4, 1.4,
"PoEAA", "PoEAA",
"2002 • M. Fowler\nRepository, Unit of Work,\nDomain Model", "2002 • M. Fowler\nRepository,"
" Unit of Work,\nDomain Model",
"white", "white",
"P", "P",
), ),
@ -354,7 +405,8 @@ def generate_catalog_map() -> None:
2.8, 2.8,
1.4, 1.4,
"Cloud\nPatterns", "Cloud\nPatterns",
"~2015 • Azure/AWS\nCircuit Breaker,\nSaga, Sidecar", "~2015 • Azure/AWS\nCircuit Breaker,\n"
"Saga, Sidecar",
GRAY1, GRAY1,
"C", "C",
), ),
@ -392,7 +444,11 @@ def generate_catalog_map() -> None:
# Mnemonic letter in corner # Mnemonic letter in corner
circle = plt.Circle( 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.add_patch(circle)
ax.text( ax.text(
@ -444,14 +500,14 @@ def generate_catalog_map() -> None:
out = str(Path(OUTPUT_DIR) / "q14_catalog_map.png") out = str(Path(OUTPUT_DIR) / "q14_catalog_map.png")
fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG) fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG)
plt.close(fig) plt.close(fig)
print(f" Saved: {out}") _logger.info(" Saved: %s", out)
# ============================================================ # ============================================================
# 3. Three Pillars of Cataloguing # 3. Three Pillars of Cataloguing
# ============================================================ # ============================================================
def generate_three_pillars() -> None: def generate_three_pillars() -> None:
"""Generate three pillars.""" """Generate three pillars of cataloguing diagram."""
fig, ax = plt.subplots(figsize=(8.27, 5.5)) fig, ax = plt.subplots(figsize=(8.27, 5.5))
ax.set_xlim(0, 12) ax.set_xlim(0, 12)
ax.set_ylim(0, 7) ax.set_ylim(0, 7)
@ -467,7 +523,13 @@ def generate_three_pillars() -> None:
# Roof / banner # Roof / banner
roof_pts = np.array([[1, 5.5], [6, 6.8], [11, 5.5]]) 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.add_patch(roof)
ax.text( ax.text(
6, 6,
@ -484,20 +546,29 @@ def generate_three_pillars() -> None:
( (
1.3, 1.3,
"1. SZABLON\nOPISU", "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", "Analogia:\nformatka\nencyklopedii",
), ),
( (
4.8, 4.8,
"2. KLASYFIKACJA\nWIELOOSIOWA", "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", "Analogia:\nkategorie\nw bibliotece",
), ),
( (
8.3, 8.3,
"3. JĘZYK\nWZORCÓW", "3. JĘZYK\nWZORCÓW",
"Wzorce referują się\nwzajemnie tworząc\nsieć/graf:\nA → wymaga → B\nB → wariant → C", "Wzorce referują się\nwzajemnie tworząc\n"
"Analogia:\n\u201ezobacz te\u017c\u201d\nw encyklopedii", "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 # Horizontal line under title
ax.plot( 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 # Description
@ -570,14 +644,19 @@ def generate_three_pillars() -> None:
out = str(Path(OUTPUT_DIR) / "q14_three_pillars.png") out = str(Path(OUTPUT_DIR) / "q14_three_pillars.png")
fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG) fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG)
plt.close(fig) plt.close(fig)
print(f" Saved: {out}") _logger.info(" Saved: %s", out)
# ============================================================ # ============================================================
# 4. Filled-in Observer Pattern Card # 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: 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)) fig, ax = plt.subplots(figsize=(8.27, 8.5))
ax.set_xlim(0, 10) ax.set_xlim(0, 10)
ax.set_ylim(0, 10) ax.set_ylim(0, 10)
@ -610,7 +689,8 @@ def generate_observer_card_filled() -> None:
( (
"P", "P",
"PROBLEM", "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" "obiektów musi zareagować, ale Subject nie\n"
"powinien znać ich konkretnych typów.", "powinien znać ich konkretnych typów.",
GRAY1, GRAY1,
@ -619,9 +699,12 @@ def generate_observer_card_filled() -> None:
( (
"Si", "Si",
"SIŁY", "SIŁY",
"• loose coupling (nie znać obserwatorów z nazwy)\n" "• loose coupling (nie znać obserwatorów"
" vs koszt powiadomień (N obserwatorów = N wywołań)\n" " z nazwy)\n"
"• otwartość na rozszerzenia vs złożoność debugowania", " vs koszt powiadomień"
" (N obserwatorów = N wywołań)\n"
"• otwartość na rozszerzenia"
" vs złożoność debugowania",
"white", "white",
False, False,
), ),
@ -651,21 +734,13 @@ def generate_observer_card_filled() -> None:
band_w = card_w - 0.6 band_w = card_w - 0.6
start_y = card_y + card_h - 0.65 start_y = card_y + card_h - 0.65
for i, (abbr, title, content, fill, is_title_field) in enumerate(fields): for i, (abbr, title, content, fill, is_title_field) in enumerate(
if is_title_field: fields
band_h = 0.7 ):
elif i == 1: band_h = _get_observer_band_height(i)
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( 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 _get_observer_band_height(j) + 0.15 for j in range(i)
for j in range(i)
) )
# Abbreviation circle # Abbreviation circle
@ -734,13 +809,24 @@ def generate_observer_card_filled() -> None:
# Arrow # Arrow
if i < len(fields) - 1: 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 info at bottom
extra_y = 0.55 extra_y = 0.55
extras = [ extras = [
"Powiązane: Mediator (centralizuje), Pub/Sub (rozproszony), MVC (View = Observer)", "Powiązane: Mediator (centralizuje),"
"Znane użycia: Java Swing listeners, C# event/delegate, React useState, DOM addEventListener", " 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): for j, txt in enumerate(extras):
ax.text( ax.text(
@ -758,14 +844,14 @@ def generate_observer_card_filled() -> None:
out = str(Path(OUTPUT_DIR) / "q14_observer_card_filled.png") out = str(Path(OUTPUT_DIR) / "q14_observer_card_filled.png")
fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG) fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG)
plt.close(fig) plt.close(fig)
print(f" Saved: {out}") _logger.info(" Saved: %s", out)
# ============================================================ # ============================================================
# 5. Pattern Language Navigation Graph # 5. Pattern Language Navigation Graph
# ============================================================ # ============================================================
def generate_pattern_language_navigation() -> None: def generate_pattern_language_navigation() -> None:
"""Generate pattern language navigation.""" """Generate pattern language navigation graph diagram."""
fig, ax = plt.subplots(figsize=(8.27, 9)) fig, ax = plt.subplots(figsize=(8.27, 9))
ax.set_xlim(0, 12) ax.set_xlim(0, 12)
ax.set_ylim(0, 12) ax.set_ylim(0, 12)
@ -773,22 +859,37 @@ def generate_pattern_language_navigation() -> None:
ax.axis("off") ax.axis("off")
fig.patch.set_facecolor(BG) fig.patch.set_facecolor(BG)
ax.set_title( 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, fontsize=FS_TITLE,
fontweight="bold", fontweight="bold",
pad=15, pad=15,
) )
# Node positions: (x, y, label, is_pattern, fill) # Node positions: (x, y, label, is_pattern, fill)
# Left column: problems; Right column: patterns
nodes = [ nodes = [
# Problems (left, rounded rect, white)
(1.5, 10.5, "Monolith\nnie skaluje się", False, "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, 8.2,
(1.5, 3.6, "Jak zachować\nspójność\ntransakcji?", False, "white"), "Jak routować\nżądania do\nserwisów?",
(1.5, 1.3, "Jak odnaleźć\nadres serwisu?", False, "white"), False, "white",
# Patterns (right, filled rect, gray) ),
(
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, 9.3, "Microservices", True, GRAY2),
(7.0, 7.0, "API Gateway", True, GRAY2), (7.0, 7.0, "API Gateway", True, GRAY2),
(7.0, 4.7, "Circuit Breaker", 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.add_patch(rect)
ax.text( 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: else:
w, h = node_w_prob, node_h_prob w, h = node_w_prob, node_h_prob
@ -841,9 +948,8 @@ def generate_pattern_language_navigation() -> None:
fontstyle="italic", fontstyle="italic",
) )
# Arrows: problem → pattern (solid), pattern → problem (dashed label) # Arrows: problem -> pattern, pattern -> problem
arrows = [ arrows = [
# (x1, y1, x2, y2, label, style)
(2.9, 10.5, 5.75, 9.5, "rozwiązuje →", "->", 1.5), (2.9, 10.5, 5.75, 9.5, "rozwiązuje →", "->", 1.5),
(7.0, 8.8, 2.9, 8.5, "← rodzi problem", "->", 1.0), (7.0, 8.8, 2.9, 8.5, "← rodzi problem", "->", 1.0),
(2.9, 8.0, 5.75, 7.2, "rozwiązuje →", "->", 1.5), (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), (2.9, 5.7, 5.75, 5.0, "rozwiązuje →", "->", 1.5),
(7.0, 4.2, 2.9, 3.9, "← rodzi problem", "->", 1.0), (7.0, 4.2, 2.9, 3.9, "← rodzi problem", "->", 1.0),
(2.9, 3.3, 5.75, 2.6, "rozwiązuje →", "->", 1.5), (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), (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), (2.9, 1.3, 8.75, 5.6, "rozwiązuje →", "->", 1.2),
] ]
@ -889,7 +993,6 @@ def generate_pattern_language_navigation() -> None:
# Legend # Legend
legend_y = 0.3 legend_y = 0.3
# Problem node
r1 = FancyBboxPatch( r1 = FancyBboxPatch(
(1.0, legend_y - 0.2), (1.0, legend_y - 0.2),
1.5, 1.5,
@ -901,8 +1004,10 @@ def generate_pattern_language_navigation() -> None:
linestyle="--", linestyle="--",
) )
ax.add_patch(r1) ax.add_patch(r1)
ax.text(1.75, legend_y, "Problem", ha="center", va="center", fontsize=7) ax.text(
# Pattern node 1.75, legend_y, "Problem",
ha="center", va="center", fontsize=7,
)
r2 = FancyBboxPatch( r2 = FancyBboxPatch(
(3.5, legend_y - 0.2), (3.5, legend_y - 0.2),
1.5, 1.5,
@ -925,7 +1030,8 @@ def generate_pattern_language_navigation() -> None:
ax.text( ax.text(
6.5, 6.5,
legend_y, legend_y,
"Nawigacja: Problem → Wzorzec → Nowy Problem → Wzorzec → ...", "Nawigacja: Problem \u2192 Wzorzec"
" \u2192 Nowy Problem \u2192 Wzorzec \u2192 ...",
ha="left", ha="left",
va="center", va="center",
fontsize=7, fontsize=7,
@ -933,20 +1039,23 @@ def generate_pattern_language_navigation() -> None:
) )
fig.tight_layout() 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) fig.savefig(out, dpi=DPI, bbox_inches="tight", facecolor=BG)
plt.close(fig) plt.close(fig)
print(f" Saved: {out}") _logger.info(" Saved: %s", out)
# ============================================================ # ============================================================
# Main # Main
# ============================================================ # ============================================================
if __name__ == "__main__": if __name__ == "__main__":
print("Generating PYTANIE 14 diagrams...") logging.basicConfig(level=logging.INFO)
_logger.info("Generating PYTANIE 14 diagrams...")
generate_pattern_template() generate_pattern_template()
generate_catalog_map() generate_catalog_map()
generate_three_pillars() generate_three_pillars()
generate_observer_card_filled() generate_observer_card_filled()
generate_pattern_language_navigation() generate_pattern_language_navigation()
print("Done!") _logger.info("Done!")

View File

@ -11,6 +11,11 @@ Diagrams:
5. ROS architecture (pub/sub nodes) 5. ROS architecture (pub/sub nodes)
""" """
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import matplotlib as mpl import matplotlib as mpl
mpl.use("Agg") mpl.use("Agg")
@ -21,6 +26,11 @@ from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
if TYPE_CHECKING:
from matplotlib.axes import Axes
_logger = logging.getLogger(__name__)
DPI = 300 DPI = 300
BG = "white" BG = "white"
LN = "black" LN = "black"
@ -38,21 +48,38 @@ WHITE = "white"
def draw_box( def draw_box(
ax, ax: Axes,
x, x: float,
y, y: float,
w, w: float,
h, h: float,
text, text: str,
fill="white", *,
lw=1.2, fill: str = "white",
fontsize=FS, lw: float = 1.2,
fontweight="normal", fontsize: float = FS,
ha="center", fontweight: str = "normal",
va="center", ha: str = "center",
rounded=True, va: str = "center",
rounded: bool = True,
) -> None: ) -> 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: if rounded:
rect = FancyBboxPatch( 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
@ -72,8 +99,29 @@ def draw_box(
) )
def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style="->", color=LN) -> None: def draw_arrow(
"""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( ax.annotate(
"", "",
xy=(x2, y2), xy=(x2, y2),
@ -101,7 +149,7 @@ def draw_trms_pyramid() -> None:
# Pyramid layers (bottom to top) # Pyramid layers (bottom to top)
layers = [ layers = [
# (y, left_x, right_x, label, sublabel, fill, examples, timing) # Fields: y left_x right_x label sublabel fill examples timing
( (
0.5, 0.5,
1.0, 1.0,
@ -261,7 +309,7 @@ def draw_trms_pyramid() -> None:
facecolor=BG, facecolor=BG,
) )
plt.close(fig) 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, facecolor=BG,
) )
plt.close(fig) plt.close(fig)
print(" robot_vendor_comparison.png") _logger.info("Generated robot_vendor_comparison.png")
# ============================================================ # ============================================================
# 3. Robot Movement Types (PTP, LIN, CIRC) # 3. Robot Movement Types (PTP, LIN, CIRC)
# ============================================================ # ============================================================
def draw_movement_types() -> None: def _draw_ptp_subplot(ax: Axes) -> None:
"""Draw movement types.""" """Draw the PTP (Point-to-Point) subplot."""
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_xlim(-0.5, 4.5)
ax.set_ylim(-0.5, 4.5) ax.set_ylim(-0.5, 4.5)
ax.set_aspect("equal") ax.set_aspect("equal")
ax.set_title("PTP (Point-to-Point)\nMoveJ / PTP", fontsize=8, fontweight="bold") ax.set_title(
ax.grid(True, alpha=0.3) "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) start = (0.5, 0.5)
end = (3.5, 3.5) end = (3.5, 3.5)
ax.plot(*start, "ko", ms=10, zorder=5) ax.plot(*start, "ko", ms=10, zorder=5)
@ -476,13 +517,18 @@ def draw_movement_types() -> None:
ax.set_ylabel("") ax.set_ylabel("")
ax.tick_params(labelsize=6) 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_xlim(-0.5, 4.5)
ax.set_ylim(-0.5, 4.5) ax.set_ylim(-0.5, 4.5)
ax.set_aspect("equal") ax.set_aspect("equal")
ax.set_title("LIN (Linear)\nMoveL / LIN", fontsize=8, fontweight="bold") ax.set_title(
ax.grid(True, alpha=0.3) "LIN (Linear)\nMoveL / LIN",
fontsize=8,
fontweight="bold",
)
ax.grid(visible=True, alpha=0.3)
start = (0.5, 1.0) start = (0.5, 1.0)
end = (3.5, 3.5) end = (3.5, 3.5)
@ -519,22 +565,27 @@ def draw_movement_types() -> None:
) )
ax.tick_params(labelsize=6) 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_xlim(-0.5, 4.5)
ax.set_ylim(-0.5, 4.5) ax.set_ylim(-0.5, 4.5)
ax.set_aspect("equal") ax.set_aspect("equal")
ax.set_title("CIRC (Circular)\nMoveC / CIRC", fontsize=8, fontweight="bold") ax.set_title(
ax.grid(True, alpha=0.3) "CIRC (Circular)\nMoveC / CIRC",
fontsize=8,
fontweight="bold",
)
ax.grid(visible=True, alpha=0.3)
# Arc through 3 points # Arc through 3 points
center = (2.0, 1.5) center = (2.0, 1.5)
r = 2.0 radius = 2.0
theta_start = np.radians(20) theta_start = np.radians(20)
theta_end = np.radians(160) theta_end = np.radians(160)
theta = np.linspace(theta_start, theta_end, 50) theta = np.linspace(theta_start, theta_end, 50)
x_circ = center[0] + r * np.cos(theta) x_circ = center[0] + radius * np.cos(theta)
y_circ = center[1] + r * np.sin(theta) y_circ = center[1] + radius * np.sin(theta)
ax.plot(x_circ, y_circ, "k-", lw=2) ax.plot(x_circ, y_circ, "k-", lw=2)
ax.annotate( 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.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[0] + 0.3, y_circ[0] - 0.3, "Start", fontsize=7)
ax.text( 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) 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) 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.tight_layout()
fig.savefig( fig.savefig(
str(Path(OUTPUT_DIR) / "robot_movement_types.png"), str(Path(OUTPUT_DIR) / "robot_movement_types.png"),
@ -576,7 +646,7 @@ def draw_movement_types() -> None:
facecolor=BG, facecolor=BG,
) )
plt.close(fig) 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( ax.text(
7.4, 7.4,
0.6, 0.6,
"✓ Bez zatrzymania produkcji\n✓ Wysoka precyzja, symulacja\n✗ Wymaga kalibracji", "✓ Bez zatrzymania produkcji\n"
"✓ Wysoka precyzja, symulacja\n"
"✗ Wymaga kalibracji",
ha="center", ha="center",
va="center", va="center",
fontsize=6.5, fontsize=6.5,
@ -679,7 +751,7 @@ def draw_online_offline() -> None:
facecolor=BG, facecolor=BG,
) )
plt.close(fig) 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 (arrows with labels)
topics = [ 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, 5.0, 4.0, 5.0, "/scan"),
(3.2, 3.0, 4.0, 3.0, "/image"), (3.2, 3.0, 4.0, 3.0, "/image"),
(6.2, 5.0, 7.0, 4.3, "/pose"), (6.2, 5.0, 7.0, 4.3, "/pose"),
@ -780,7 +852,7 @@ def draw_ros_architecture() -> None:
facecolor=BG, facecolor=BG,
) )
plt.close(fig) 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, 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)"), (2.5, "SetDO = Digital Output\nSterowanie I/O\n(chwytak, zawory)"),
] ]
@ -925,18 +999,19 @@ def draw_rapid_structure() -> None:
facecolor=BG, facecolor=BG,
) )
plt.close(fig) plt.close(fig)
print(" robot_rapid_example.png") _logger.info("Generated robot_rapid_example.png")
# ============================================================ # ============================================================
# Main # Main
# ============================================================ # ============================================================
if __name__ == "__main__": if __name__ == "__main__":
print("Generating PYTANIE 16 diagrams...") logging.basicConfig(level=logging.INFO)
_logger.info("Generating PYTANIE 16 diagrams...")
draw_trms_pyramid() draw_trms_pyramid()
draw_vendor_comparison() draw_vendor_comparison()
draw_movement_types() draw_movement_types()
draw_online_offline() draw_online_offline()
draw_ros_architecture() draw_ros_architecture()
draw_rapid_structure() draw_rapid_structure()
print("Done! All diagrams saved to", OUTPUT_DIR) _logger.info("Done! All diagrams saved to %s", OUTPUT_DIR)

View File

@ -1,14 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Generate diagrams for PYTANIE 2: Shortest path algorithms. """Generate diagrams for PYTANIE 2: Shortest path algorithms.
1. Graph structure the shared example graph (A,B,C,D) 1. Graph structure -- the shared example graph (A,B,C,D)
2. Dijkstra traversal step-by-step on that graph 2. Dijkstra traversal -- step-by-step on that graph
3. Bellman-Ford traversal step-by-step 3. Bellman-Ford traversal -- step-by-step
4. A* traversal step-by-step with heuristics. 4. A* traversal -- step-by-step with heuristics.
All: A4-compatible, B&W, 300 DPI, laser-printer-friendly. 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 import matplotlib as mpl
mpl.use("Agg") mpl.use("Agg")
@ -17,47 +22,65 @@ from pathlib import Path
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
if TYPE_CHECKING:
from matplotlib.axes import Axes
_logger = logging.getLogger(__name__)
DPI = 300 DPI = 300
BG = "white" BG = "white"
LN = "black" LN = "black"
FS = 8 FS = 8
FS_TITLE = 11 FS_TITLE = 11
FS_SMALL = 6.5
FS_EDGE = 9 FS_EDGE = 9
OUTPUT_DIR = str(Path(__file__).resolve().parent / "img") OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True) Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
GRAY1 = "#E8E8E8"
GRAY2 = "#D0D0D0"
GRAY3 = "#B8B8B8" GRAY3 = "#B8B8B8"
GRAY4 = "#F5F5F5" GRAY4 = "#F5F5F5"
GRAY5 = "#C0C0C0"
LIGHT_GREEN = "#D5E8D4" LIGHT_GREEN = "#D5E8D4"
LIGHT_RED = "#F8D7DA"
LIGHT_BLUE = "#D6EAF8" LIGHT_BLUE = "#D6EAF8"
LIGHT_YELLOW = "#FFF9C4" LIGHT_YELLOW = "#FFF9C4"
LIGHT_ORANGE = "#FFE0B2"
LIGHT_PURPLE = "#E8D5F5"
# --- Shared graph layout --- NODE_POS: dict[str, tuple[float, float]] = {
# Graph: A--2--B--3--D, A--4--C--5--D, D--1--C (directed: C->D has weight 5) "A": (1, 2),
NODE_POS = {"A": (1, 2), "B": (3.5, 3.2), "C": (1, 0), "D": (3.5, 0.8)} "B": (3.5, 3.2),
EDGES = [("A", "B", 2), ("A", "C", 4), ("B", "D", 3), ("C", "D", 5)] "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( def draw_graph_node(
ax, ax: Axes,
name, name: str,
pos, pos: tuple[float, float],
color="white", *,
current=False, color: str = "white",
visited=False, current: bool = False,
dist_label=None, visited: bool = False,
fontsize=12, dist_label: str | None = None,
fontsize: int = 12,
) -> None: ) -> 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 x, y = pos
r = 0.35 radius = 0.35
lw = 2.5 if current else 1.5 lw = 2.5 if current else 1.5
ec = "#D32F2F" if current else LN ec = "#D32F2F" if current else LN
fc = LIGHT_GREEN if visited else color fc = LIGHT_GREEN if visited else color
@ -65,7 +88,13 @@ def draw_graph_node(
fc = LIGHT_YELLOW fc = LIGHT_YELLOW
circle = plt.Circle( 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.add_patch(circle)
ax.text( ax.text(
@ -97,25 +126,52 @@ def draw_graph_node(
) )
def draw_graph_edge(ax, pos1, pos2, weight, highlighted=False, relaxed=False) -> None: def draw_graph_edge(
"""Draw an edge between two nodes with weight label.""" 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 x1, y1 = pos1
x2, y2 = pos2 x2, y2 = pos2
# Shorten line to not overlap node circles # Shorten line to not overlap node circles
dx, dy = x2 - x1, y2 - y1 dx, dy = x2 - x1, y2 - y1
length = np.sqrt(dx**2 + dy**2) length = np.sqrt(dx**2 + dy**2)
r = 0.38 node_radius = 0.38
sx = x1 + r * dx / length sx = x1 + node_radius * dx / length
sy = y1 + r * dy / length sy = y1 + node_radius * dy / length
ex = x2 - r * dx / length ex = x2 - node_radius * dx / length
ey = y2 - r * dy / 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 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 # Weight label
mx = (x1 + x2) / 2 mx = (x1 + x2) / 2
@ -135,7 +191,9 @@ def draw_graph_edge(ax, pos1, pos2, weight, highlighted=False, relaxed=False) ->
bbox={ bbox={
"boxstyle": "round,pad=0.15", "boxstyle": "round,pad=0.15",
"facecolor": "white", "facecolor": "white",
"edgecolor": GRAY3 if not highlighted else color, "edgecolor": (
GRAY3 if not highlighted else color
),
"alpha": 0.95, "alpha": 0.95,
}, },
zorder=4, zorder=4,
@ -143,15 +201,26 @@ def draw_graph_edge(ax, pos1, pos2, weight, highlighted=False, relaxed=False) ->
def draw_full_graph( def draw_full_graph(
ax, ax: Axes,
title="", *,
dist=None, title: str = "",
current=None, dist: dict[str, str] | None = None,
visited=None, current: str | None = None,
highlighted_edges=None, visited: set[str] | None = None,
relaxed_edges=None, highlighted_edges: set[tuple[str, str]] | None = None,
relaxed_edges: set[tuple[str, str]] | None = 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: if visited is None:
visited = set() visited = set()
if highlighted_edges is None: if highlighted_edges is None:
@ -170,17 +239,35 @@ def draw_full_graph(
# Draw edges # Draw edges
for u, v, w in EDGES: for u, v, w in EDGES:
hl = (u, v) in highlighted_edges or (v, u) in highlighted_edges hl = (
rl = (u, v) in relaxed_edges or (v, u) in relaxed_edges (u, v) in highlighted_edges
draw_graph_edge(ax, NODE_POS[u], NODE_POS[v], w, highlighted=hl, relaxed=rl) 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 # Draw nodes
for name, pos in NODE_POS.items(): for node_name, pos in NODE_POS.items():
is_current = name == current is_current = node_name == current
is_visited = name in visited is_visited = node_name in visited
d_label = dist.get(name) d_label = dist.get(node_name)
draw_graph_node( 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 # 1. Graph structure diagram
# ============================================================ # ============================================================
def draw_graph_structure() -> None: 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)) _fig, ax = plt.subplots(1, 1, figsize=(5, 4))
ax.set_xlim(-0.5, 5.0) ax.set_xlim(-0.5, 5.0)
ax.set_ylim(-1.2, 4.5) 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_graph_edge(ax, NODE_POS[u], NODE_POS[v], w)
# Draw nodes # Draw nodes
for name, pos in NODE_POS.items(): for node_name, pos in NODE_POS.items():
draw_graph_node(ax, name, pos) draw_graph_node(ax, node_name, pos)
# Start arrow # Start arrow
ax.annotate( ax.annotate(
@ -241,14 +328,14 @@ def draw_graph_structure() -> None:
facecolor=BG, facecolor=BG,
) )
plt.close() plt.close()
print("graph_example_structure.png") _logger.info("graph_example_structure.png")
# ============================================================ # ============================================================
# 2. Dijkstra traversal # 2. Dijkstra traversal
# ============================================================ # ============================================================
def draw_dijkstra_traversal() -> None: def draw_dijkstra_traversal() -> None:
"""Step-by-step Dijkstra on the shared graph.""" """Draw step-by-step Dijkstra on the shared graph."""
steps = [ steps = [
{ {
"title": "Krok 0: Inicjalizacja\nd = {A:0, B:∞, C:∞, D:∞}", "title": "Krok 0: Inicjalizacja\nd = {A:0, B:∞, C:∞, D:∞}",
@ -259,7 +346,11 @@ def draw_dijkstra_traversal() -> None:
"relaxed": set(), "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": ""}, "dist": {"A": "0", "B": "2", "C": "4", "D": ""},
"current": "A", "current": "A",
"visited": {"A"}, "visited": {"A"},
@ -267,7 +358,11 @@ def draw_dijkstra_traversal() -> None:
"relaxed": {("A", "B"), ("A", "C")}, "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"}, "dist": {"A": "0", "B": "2", "C": "4", "D": "5"},
"current": "B", "current": "B",
"visited": {"A", "B"}, "visited": {"A", "B"},
@ -275,7 +370,11 @@ def draw_dijkstra_traversal() -> None:
"relaxed": {("B", "D")}, "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"}, "dist": {"A": "0", "B": "2", "C": "4", "D": "5"},
"current": "C", "current": "C",
"visited": {"A", "B", "C"}, "visited": {"A", "B", "C"},
@ -283,7 +382,11 @@ def draw_dijkstra_traversal() -> None:
"relaxed": set(), "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"}, "dist": {"A": "0", "B": "2", "C": "4", "D": "5"},
"current": None, "current": None,
"visited": {"A", "B", "C", "D"}, "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, axes = plt.subplots(1, 5, figsize=(14, 3.5))
fig.suptitle( 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, fontsize=FS_TITLE,
fontweight="bold", fontweight="bold",
y=1.02, y=1.02,
@ -315,11 +419,17 @@ def draw_dijkstra_traversal() -> None:
fig.text( fig.text(
0.5, 0.5,
-0.04, -0.04,
"[zolty] = aktualnie przetwarzany [zielony] = odwiedzony (zamkniety) " "[zolty] = aktualnie przetwarzany"
"czerwona krawedz = relaksacja OK szara krawedz = nie poprawia", " [zielony] = odwiedzony (zamkniety)"
" czerwona krawedz = relaksacja OK"
" szara krawedz = nie poprawia",
ha="center", ha="center",
fontsize=FS, 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() plt.tight_layout()
@ -330,18 +440,19 @@ def draw_dijkstra_traversal() -> None:
facecolor=BG, facecolor=BG,
) )
plt.close() plt.close()
print("dijkstra_traversal.png") _logger.info("dijkstra_traversal.png")
# ============================================================ # ============================================================
# 3. Bellman-Ford traversal # 3. Bellman-Ford traversal
# ============================================================ # ============================================================
def draw_bellman_ford_traversal() -> None: 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 = plt.figure(figsize=(14, 7))
fig.suptitle( fig.suptitle(
"Bellman-Ford — przejście grafu krok po kroku\n" "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, fontsize=FS_TITLE,
fontweight="bold", fontweight="bold",
y=0.98, y=0.98,
@ -408,14 +519,23 @@ def draw_bellman_ford_traversal() -> None:
) )
# Negative cycle check note # Negative cycle check note
neg_cycle_msg = (
"Po 3 iteracjach: sprawdz raz jeszcze"
" — nic sie nie zmienia"
" → BRAK cyklu ujemnego → wynik poprawny"
)
fig.text( fig.text(
0.5, 0.5,
0.01, 0.01,
"Po 3 iteracjach: sprawdź raz jeszcze — nic się nie zmienia → BRAK cyklu ujemnego → wynik poprawny", neg_cycle_msg,
ha="center", ha="center",
fontsize=FS, fontsize=FS,
fontweight="bold", 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]) plt.tight_layout(rect=[0, 0.05, 1, 0.95])
@ -426,21 +546,22 @@ def draw_bellman_ford_traversal() -> None:
facecolor=BG, facecolor=BG,
) )
plt.close() plt.close()
print("bellman_ford_traversal.png") _logger.info("bellman_ford_traversal.png")
# ============================================================ # ============================================================
# 4. A* traversal # 4. A* traversal
# ============================================================ # ============================================================
def draw_astar_traversal() -> None: 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) # Heuristic values (straight-line distance to D)
h_vals = {"A": 4, "B": 2, "C": 3, "D": 0} h_vals = {"A": 4, "B": 2, "C": 3, "D": 0}
fig = plt.figure(figsize=(14, 7.5)) fig = plt.figure(figsize=(14, 7.5))
fig.suptitle( fig.suptitle(
"A* — przejście grafu krok po kroku (cel = D)\n" "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, fontsize=FS_TITLE,
fontweight="bold", fontweight="bold",
y=0.99, y=0.99,
@ -520,11 +641,11 @@ def draw_astar_traversal() -> None:
) )
# Add h values as small labels # Add h values as small labels
for name, pos in NODE_POS.items(): for node_name, pos in NODE_POS.items():
ax_g.text( ax_g.text(
pos[0] + 0.35, pos[0] + 0.35,
pos[1] + 0.35, pos[1] + 0.35,
f"h={h_vals[name]}", f"h={h_vals[node_name]}",
ha="center", ha="center",
va="center", va="center",
fontsize=5.5, fontsize=5.5,
@ -558,8 +679,11 @@ def draw_astar_traversal() -> None:
fig.text( fig.text(
0.5, 0.5,
0.01, 0.01,
"A* odwiedził 3 wierzchołki (A, B, D) — POMINĄŁ C!\n" "A* odwiedził 3 wierzchołki (A, B, D)"
"Dijkstra odwiedziłby wszystkie 4. Heurystyka h kieruje przeszukiwanie w stronę celu.", " — POMINĄŁ C!\n"
"Dijkstra odwiedziłby wszystkie 4."
" Heurystyka h kieruje przeszukiwanie"
" w stronę celu.",
ha="center", ha="center",
fontsize=FS, fontsize=FS,
fontweight="bold", fontweight="bold",
@ -578,16 +702,17 @@ def draw_astar_traversal() -> None:
facecolor=BG, facecolor=BG,
) )
plt.close() plt.close()
print("astar_traversal.png") _logger.info("astar_traversal.png")
# ============================================================ # ============================================================
# Main # Main
# ============================================================ # ============================================================
if __name__ == "__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_graph_structure()
draw_dijkstra_traversal() draw_dijkstra_traversal()
draw_bellman_ford_traversal() draw_bellman_ford_traversal()
draw_astar_traversal() draw_astar_traversal()
print(f"\nAll diagrams saved to {OUTPUT_DIR}/") _logger.info("All diagrams saved to %s/", OUTPUT_DIR)

View File

@ -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. Placed in pytania/questions/ folder.
""" """
from __future__ import annotations
import logging
from pathlib import Path from pathlib import Path
import re import re
@ -13,6 +16,9 @@ SOURCE = str(Path(SCRIPT_DIR) / "OBRONA_MAGISTERSKA_ODPOWIEDZI.md")
OUT_DIR = str(Path(SCRIPT_DIR) / "questions") OUT_DIR = str(Path(SCRIPT_DIR) / "questions")
Path(OUT_DIR).mkdir(parents=True, exist_ok=True) 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: with Path(SOURCE).open(encoding="utf-8") as f:
lines = f.readlines() lines = f.readlines()
@ -25,7 +31,7 @@ for i, line in enumerate(lines):
title = m.group(3).strip() title = m.group(3).strip()
question_starts.append((i, raw_num, title)) 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): for idx, (start_line, raw_num, title) in enumerate(question_starts):
# End = next question start or EOF # 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) f.writelines(content_lines)
line_count = len(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)