praca_magisterska/pytania/generate_bf_negative_diagram.py

382 lines
14 KiB
Python
Raw Normal View History

2026-02-21 19:51:31 +01:00
#!/usr/bin/env python3
"""
Generate Bellman-Ford negative-weights & negative-cycle diagram for PYTANIE 2.
Two-part figure:
Part 1: Graph with negative edge, Dijkstra WRONG vs Bellman-Ford CORRECT
Part 2: Negative cycle detection (add CB(3))
A4-compatible, monochrome-friendly, 300 DPI.
"""
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import os
DPI = 300
BG = 'white'
LN = 'black'
FS = 8
FS_TITLE = 10
FS_SMALL = 6.5
FS_EDGE = 9
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'img')
os.makedirs(OUTPUT_DIR, exist_ok=True)
GRAY1 = '#E8E8E8'
GRAY2 = '#D0D0D0'
GRAY3 = '#B8B8B8'
GRAY4 = '#F5F5F5'
LIGHT_GREEN = '#D5E8D4'
LIGHT_RED = '#F8D7DA'
LIGHT_YELLOW = '#FFF9C4'
# --- Graph layout for negative-weight example ---
# S→A(2), A→C(3), S→B(5), B→A(4)
NEG_POS = {'S': (0.8, 2), 'A': (3.3, 3.2), 'B': (3.3, 0.8), 'C': (5.8, 2)}
NEG_EDGES = [('S', 'A', 2), ('A', 'C', 3), ('S', 'B', 5), ('B', 'A', -4)]
def draw_node(ax, name, pos, color='white', current=False, visited=False,
dist_label=None, fontsize=12, error=False):
x, y = pos
r = 0.35
lw = 2.5 if current else 1.5
ec = '#D32F2F' if current else ('#D32F2F' if error else LN)
fc = LIGHT_YELLOW if current else (LIGHT_GREEN if visited else color)
if error:
fc = LIGHT_RED
circle = plt.Circle((x, y), r, fill=True, facecolor=fc,
edgecolor=ec, linewidth=lw, zorder=5)
ax.add_patch(circle)
ax.text(x, y, name, ha='center', va='center', fontsize=fontsize,
fontweight='bold', zorder=6)
if dist_label is not None:
bbox_ec = '#D32F2F' if error else GRAY3
bbox_fc = LIGHT_RED if error else 'white'
ax.text(x, y - 0.55, f'd={dist_label}', ha='center', va='center',
fontsize=FS, zorder=6,
bbox=dict(boxstyle='round,pad=0.15', facecolor=bbox_fc,
edgecolor=bbox_ec, alpha=0.95))
def draw_edge(ax, pos1, pos2, weight, highlighted=False, relaxed=False,
negative=False, cycle_edge=False, offset=0.0):
x1, y1 = pos1
x2, y2 = pos2
dx, dy = x2 - x1, y2 - y1
length = np.sqrt(dx**2 + dy**2)
r = 0.38
sx = x1 + r * dx / length
sy = y1 + r * dy / length
ex = x2 - r * dx / length
ey = y2 - r * dy / length
# Offset perpendicular for parallel edges
if offset != 0:
perp_x = -dy / length * offset
perp_y = dx / length * offset
sx += perp_x; sy += perp_y
ex += perp_x; ey += perp_y
if cycle_edge:
color = '#D32F2F'
lw = 2.5
ls = '--'
elif negative:
color = '#D32F2F'
lw = 2.5
ls = '-'
elif relaxed:
color = '#D32F2F'
lw = 2.5
ls = '-'
elif highlighted:
color = '#1565C0'
lw = 2.0
ls = '-'
else:
color = GRAY3
lw = 1.5
ls = '-'
# Arrow
ax.annotate('', xy=(ex, ey), xytext=(sx, sy),
arrowprops=dict(arrowstyle='->', color=color, lw=lw,
linestyle=ls, shrinkA=0, shrinkB=0),
zorder=2)
# Weight label
mx = (sx + ex) / 2
my = (sy + ey) / 2
perp_x = -dy / length * 0.22
perp_y = dx / length * 0.22
if offset != 0:
perp_x *= 0.5
perp_y *= 0.5
weight_str = str(weight)
edge_fc = LIGHT_RED if negative or cycle_edge else 'white'
edge_ec = '#D32F2F' if negative or cycle_edge else GRAY3
ax.text(mx + perp_x, my + perp_y, weight_str, ha='center', va='center',
fontsize=FS_EDGE, fontweight='bold',
bbox=dict(boxstyle='round,pad=0.15', facecolor=edge_fc,
edgecolor=edge_ec, alpha=0.95),
zorder=4)
def draw_neg_graph(ax, edges, title="", dist=None, current=None, visited=None,
relaxed_edges=None, error_nodes=None, extra_edges=None,
node_positions=None):
if visited is None:
visited = set()
if relaxed_edges is None:
relaxed_edges = set()
if dist is None:
dist = {}
if error_nodes is None:
error_nodes = set()
if node_positions is None:
node_positions = NEG_POS
ax.set_xlim(-0.5, 7.0)
ax.set_ylim(-0.8, 4.5)
ax.set_aspect('equal')
ax.axis('off')
if title:
ax.set_title(title, fontsize=FS, fontweight='bold', pad=5)
all_edges = list(edges)
if extra_edges:
all_edges += extra_edges
for u, v, w in all_edges:
rl = (u, v) in relaxed_edges
neg = w < 0
cycle = extra_edges and (u, v, w) in extra_edges
# If B→A and A→B both exist, offset them
off = 0.0
draw_edge(ax, node_positions[u], node_positions[v], w,
relaxed=rl, negative=neg, cycle_edge=cycle, offset=off)
for name, pos in node_positions.items():
is_current = (name == current)
is_visited = (name in visited)
d_label = dist.get(name, None)
is_error = (name in error_nodes)
draw_node(ax, name, pos, current=is_current, visited=is_visited,
dist_label=d_label, error=is_error)
def generate_bf_negative_weights():
"""
Two-row figure:
Row 1: Graph structure + Dijkstra WRONG + Bellman-Ford CORRECT
Row 2: B-F iterations 1-3 step by step
"""
fig = plt.figure(figsize=(14, 10))
fig.suptitle('Bellman-Ford — ujemne wagi vs Dijkstra\n'
'Graf: S→A(2), A→C(3), S→B(5), B→A(4). Start = S',
fontsize=FS_TITLE + 1, fontweight='bold', y=0.99)
# ---- Row 1: Graph + Dijkstra wrong + BF correct ----
# Panel 1: The graph structure
ax1 = fig.add_subplot(2, 3, 1)
draw_neg_graph(ax1, NEG_EDGES,
title='Graf z ujemną wagą\n(B→A = 4, zaznaczona na czerwono)',
dist={'S': '0', 'A': '?', 'B': '?', 'C': '?'})
# START label
ax1.annotate("START", xy=(NEG_POS['S'][0] - 0.35, NEG_POS['S'][1]),
xytext=(NEG_POS['S'][0] - 1.2, NEG_POS['S'][1]),
fontsize=FS, fontweight='bold', color='#D32F2F',
arrowprops=dict(arrowstyle='->', color='#D32F2F', lw=2),
va='center')
# Panel 2: Dijkstra — WRONG
ax2 = fig.add_subplot(2, 3, 2)
draw_neg_graph(ax2, NEG_EDGES,
title='Dijkstra — BŁĘDNY wynik\nA zamknięty z d=2, nie poprawia przy B→A',
dist={'S': '0', 'A': '2', 'B': '5', 'C': '5'},
visited={'S', 'A', 'B', 'C'},
error_nodes={'A', 'C'})
# Add "WRONG" annotations
ax2.text(NEG_POS['A'][0] + 0.6, NEG_POS['A'][1] + 0.3, '✗ powinno 1',
fontsize=FS_SMALL, color='#D32F2F', fontweight='bold',
bbox=dict(boxstyle='round,pad=0.1', facecolor=LIGHT_RED,
edgecolor='#D32F2F', alpha=0.9, lw=0.5))
ax2.text(NEG_POS['C'][0] + 0.05, NEG_POS['C'][1] + 0.55, '✗ powinno 4',
fontsize=FS_SMALL, color='#D32F2F', fontweight='bold',
bbox=dict(boxstyle='round,pad=0.1', facecolor=LIGHT_RED,
edgecolor='#D32F2F', alpha=0.9, lw=0.5))
# Panel 3: Bellman-Ford — CORRECT
ax3 = fig.add_subplot(2, 3, 3)
draw_neg_graph(ax3, NEG_EDGES,
title='Bellman-Ford — POPRAWNY wynik\nUjemna waga B→A poprawnie propagowana',
dist={'S': '0', 'A': '1', 'B': '5', 'C': '4'},
visited={'S', 'A', 'B', 'C'},
relaxed_edges={('B', 'A')})
ax3.text(NEG_POS['A'][0] + 0.6, NEG_POS['A'][1] + 0.3, '✓ poprawne!',
fontsize=FS_SMALL, color='#006400', fontweight='bold',
bbox=dict(boxstyle='round,pad=0.1', facecolor=LIGHT_GREEN,
edgecolor='#006400', alpha=0.9, lw=0.5))
ax3.text(NEG_POS['C'][0] + 0.05, NEG_POS['C'][1] + 0.55, '✓ poprawne!',
fontsize=FS_SMALL, color='#006400', fontweight='bold',
bbox=dict(boxstyle='round,pad=0.1', facecolor=LIGHT_GREEN,
edgecolor='#006400', alpha=0.9, lw=0.5))
# ---- Row 2: B-F iterations step by step ----
iterations = [
{
'title': 'B-F Iteracja 1\nRelaksuj WSZYSTKIE krawędzie',
'dist': {'S': '0', 'A': '1', 'B': '5', 'C': '5'},
'relaxed': {('S', 'A'), ('A', 'C'), ('S', 'B'), ('B', 'A')},
'detail': ('S→A: 0+2=2<∞ → A=2\n'
'A→C: 2+3=5<∞ → C=5\n'
'S→B: 0+5=5<∞ → B=5\n'
'B→A: 54=1<2 → A=1 ✓'),
},
{
'title': 'B-F Iteracja 2\nPropagacja poprawionego A',
'dist': {'S': '0', 'A': '1', 'B': '5', 'C': '4'},
'relaxed': {('A', 'C')},
'detail': ('S→A: 0+2=2>1 ✗\n'
'A→C: 1+3=4<5 → C=4 ✓\n'
'S→B: 0+5=5=5 ✗\n'
'B→A: 54=1=1 ✗'),
},
{
'title': 'B-F Iteracja 3\nBrak zmian → stabilne!',
'dist': {'S': '0', 'A': '1', 'B': '5', 'C': '4'},
'relaxed': set(),
'detail': ('Wszystkie krawędzie:\n'
'brak poprawy ✗\n'
'→ wynik stabilny\n'
'→ BRAK cyklu ujemnego'),
},
]
for i, it in enumerate(iterations):
ax = fig.add_subplot(2, 3, i + 4)
draw_neg_graph(ax, NEG_EDGES, title=it['title'],
dist=it['dist'],
visited={'S', 'A', 'B', 'C'},
relaxed_edges=it['relaxed'])
# Detail text below graph
ax.text(3.2, -0.5, it['detail'], ha='center', va='top',
fontsize=FS_SMALL, family='monospace',
bbox=dict(boxstyle='round,pad=0.3', facecolor=GRAY4,
edgecolor=GRAY3))
# Bottom note
fig.text(0.5, 0.01,
'Dijkstra zamyka wierzchołki na stałe (zachłanność) → ujemna waga B→A(4) nie może poprawić zamkniętego A.\n'
'Bellman-Ford relaksuje WSZYSTKIE krawędzie w każdej iteracji → ujemne wagi propagują się poprawnie.',
ha='center', fontsize=FS, fontweight='bold',
bbox=dict(boxstyle='round,pad=0.3', facecolor=LIGHT_YELLOW, edgecolor=LN))
plt.tight_layout(rect=[0, 0.05, 1, 0.95])
plt.savefig(os.path.join(OUTPUT_DIR, 'bellman_ford_negative_weights.png'),
dpi=DPI, bbox_inches='tight', facecolor=BG)
plt.close()
print(" ✓ bellman_ford_negative_weights.png")
def generate_bf_negative_cycle():
"""
Figure showing negative cycle detection.
Graph: SA(2), AC(3), SB(5), BA(4), CB(3) [added edge]
Cycle: BACB = 4+3+(3) = 4 < 0
"""
cycle_edges = NEG_EDGES + [('C', 'B', -3)]
fig = plt.figure(figsize=(14, 5.5))
fig.suptitle('Bellman-Ford — wykrywanie cyklu ujemnego\n'
'Dodano krawędź C→B(3). Cykl: B→A→C→B = 4+3+(3) = 4 < 0',
fontsize=FS_TITLE + 1, fontweight='bold', y=0.99)
# Panel 1: Graph with cycle highlighted
ax1 = fig.add_subplot(1, 3, 1)
draw_neg_graph(ax1, NEG_EDGES,
title='Graf z cyklem ujemnym\nDodana krawędź C→B(3) — przerywana',
dist={'S': '0', 'A': '?', 'B': '?', 'C': '?'},
extra_edges=[('C', 'B', -3)])
# Mark cycle
ax1.annotate("CYKL\n4+3+(3)=4<0",
xy=(3.3, 2.0),
fontsize=FS, fontweight='bold', color='#D32F2F',
ha='center', va='center',
bbox=dict(boxstyle='round,pad=0.3', facecolor=LIGHT_RED,
edgecolor='#D32F2F', alpha=0.9))
# Panel 2: After V1 iterations — still changing
ax2 = fig.add_subplot(1, 3, 2)
draw_neg_graph(ax2, NEG_EDGES,
title='Po V1=3 iteracjach\ndist wciąż maleje (niestabilne!)',
dist={'S': '0', 'A': '7', 'B': '4', 'C': '4'},
visited={'S', 'A', 'B', 'C'},
error_nodes={'A', 'B', 'C'},
extra_edges=[('C', 'B', -3)])
ax2.text(3.2, -0.4,
'Każde okrążenie cyklu\nzmniejsza dist o 4.\n'
'Dist → −∞ (brak minimum!)',
ha='center', va='top', fontsize=FS_SMALL, fontweight='bold',
bbox=dict(boxstyle='round,pad=0.3', facecolor=LIGHT_RED,
edgecolor='#D32F2F'))
# Panel 3: V-th iteration detects
ax3 = fig.add_subplot(1, 3, 3)
ax3.axis('off')
ax3.set_xlim(0, 10)
ax3.set_ylim(0, 10)
detection_text = (
"V-ta iteracja (sprawdzenie):\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"for (src, dst, w) in edges:\n"
" if dist[src]+w < dist[dst]:\n"
" return None # CYKL!\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"Sprawdzamy np. krawędź B→A:\n"
" dist[B] + (4) = 4 + (4) = 8\n"
" 8 < dist[A] = 7\n"
" → NADAL SIĘ POPRAWIA!\n"
" → CYKL UJEMNY WYKRYTY!\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"Wynik: return None\n"
"(najkrótsza ścieżka nie istnieje)"
)
ax3.text(5, 5, detection_text, ha='center', va='center',
fontsize=FS + 0.5, family='monospace',
bbox=dict(boxstyle='round,pad=0.6', facecolor=LIGHT_RED,
edgecolor='#D32F2F', lw=2))
ax3.set_title('Wykrywanie — V-ta iteracja\nJeśli cokolwiek się poprawia → cykl ujemny!',
fontsize=FS, fontweight='bold', pad=5)
# Bottom note
fig.text(0.5, 0.01,
'Bez cyklu ujemnego: po V1 iteracjach dist jest stabilne. '
'Z cyklem ujemnym: dist maleje w nieskończoność → V-ta iteracja to wykrywa.',
ha='center', fontsize=FS, fontweight='bold',
bbox=dict(boxstyle='round,pad=0.3', facecolor=LIGHT_YELLOW, edgecolor=LN))
plt.tight_layout(rect=[0, 0.06, 1, 0.94])
plt.savefig(os.path.join(OUTPUT_DIR, 'bellman_ford_negative_cycle.png'),
dpi=DPI, bbox_inches='tight', facecolor=BG)
plt.close()
print(" ✓ bellman_ford_negative_cycle.png")
if __name__ == '__main__':
print("Generating Bellman-Ford negative weight diagrams...")
generate_bf_negative_weights()
generate_bf_negative_cycle()
print(f"\nAll diagrams saved to {OUTPUT_DIR}/")