"""MoviePy visualization for PYTANIE 2: Shortest path algorithms. Creates an animated video walking through Dijkstra, Bellman-Ford, and A* on a small example graph, rendering each algorithm step by step. """ from __future__ import annotations import os from pathlib import Path import numpy as np os.environ["FFMPEG_BINARY"] = "/usr/bin/ffmpeg" from moviepy import ( ColorClip, CompositeVideoClip, TextClip, VideoClip, concatenate_videoclips, ) from moviepy.video.fx import FadeIn, FadeOut # ── Constants ───────────────────────────────────────────────────── W, H = 1280, 720 FPS = 24 STEP_DUR = 8.0 HEADER_DUR = 5.0 FONT_B = "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf" FONT_R = "/usr/share/fonts/TTF/DejaVuSans.ttf" OUTPUT_DIR = Path(__file__).resolve().parent / "videos" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) OUTPUT = str(OUTPUT_DIR / "q02_shortest_path.mp4") # Graph definition NODE_POS = {"S": (250, 280), "A": (550, 180), "B": (550, 450), "C": (850, 320)} EDGES_DIJKSTRA = [ ("S", "A", 2), ("S", "B", 5), ("A", "C", 3), ("B", "A", 1), ("B", "C", 6), ] EDGES_BF = [("S", "A", 2), ("A", "C", 3), ("S", "B", 5), ("B", "A", -4)] # Colors BG = (20, 20, 40) COL_DEFAULT = (70, 130, 200) COL_CURRENT = (255, 200, 50) COL_VISITED = (80, 200, 100) COL_EDGE = (100, 100, 130) COL_EDGE_ACT = (255, 100, 80) INF = "inf" def _tc(**kwargs: object) -> TextClip: """TextClip wrapper that adds enough bottom margin to prevent clipping.""" fs = kwargs.get("font_size", 24) m = int(fs) // 3 + 2 kwargs["margin"] = (0, m) return TextClip(**kwargs) def _make_header( title: str, subtitle: str, duration: float = HEADER_DUR ) -> CompositeVideoClip: bg = ColorClip(size=(W, H), color=BG).with_duration(duration) t = ( _tc( text=title, font_size=52, color="white", font=FONT_B, ) .with_duration(duration) .with_position(("center", 250)) ) s = ( _tc( text=subtitle, font_size=28, color="#AABBCC", font=FONT_R, ) .with_duration(duration) .with_position(("center", 340)) ) return CompositeVideoClip([bg, t, s], size=(W, H)).with_effects( [FadeIn(0.5), FadeOut(0.5)] ) def _draw_circle( frame: np.ndarray, cx: int, cy: int, r: int, color: tuple[int, ...] ) -> None: yy, xx = np.ogrid[:H, :W] mask = ((xx - cx) ** 2 + (yy - cy) ** 2) <= r**2 frame[mask] = color def _draw_line( frame: np.ndarray, x1: int, y1: int, x2: int, y2: int, color: tuple[int, ...], thickness: int = 2, ) -> None: length = max(int(np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)), 1) for i in range(length): frac = i / length px = int(x1 + frac * (x2 - x1)) py = int(y1 + frac * (y2 - y1)) for dx in range(-thickness, thickness + 1): for dy in range(-thickness, thickness + 1): nx, ny = px + dx, py + dy if 0 <= nx < W and 0 <= ny < H: frame[ny, nx] = color def _draw_arrow( frame: np.ndarray, x1: int, y1: int, x2: int, y2: int, color: tuple[int, ...], thickness: int = 2, ) -> None: r = 32 length = max(np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2), 1) ddx = (x2 - x1) / length ddy = (y2 - y1) / length sx = int(x1 + ddx * r) sy = int(y1 + ddy * r) ex = int(x2 - ddx * r) ey = int(y2 - ddy * r) _draw_line(frame, sx, sy, ex, ey, color, thickness) angle = np.arctan2(ey - sy, ex - sx) arrow_len = 12 for side in [-1, 1]: a = angle + np.pi + side * 0.4 ax = int(ex + arrow_len * np.cos(a)) ay = int(ey + arrow_len * np.sin(a)) _draw_line(frame, ex, ey, ax, ay, color, thickness) def _render_graph( nodes: dict[str, tuple[int, int]], edges: list[tuple[str, str, int]], _distances: dict[str, str], current: str | None = None, visited: set[str] | None = None, active_edge: tuple[str, str] | None = None, ) -> np.ndarray: if visited is None: visited = set() frame = np.full((H, W, 3), BG, dtype=np.uint8) for src, dst, _w in edges: sx, sy = nodes[src] dx, dy = nodes[dst] ec = COL_EDGE_ACT if active_edge == (src, dst) else COL_EDGE _draw_arrow(frame, sx, sy, dx, dy, ec, thickness=2) for name, (x, y) in nodes.items(): if name == current: nc = COL_CURRENT elif name in visited: nc = COL_VISITED else: nc = COL_DEFAULT _draw_circle(frame, x, y, 30, nc) # Border ring border = tuple(max(c - 40, 0) for c in nc) yy, xx = np.ogrid[:H, :W] ring = (((xx - x) ** 2 + (yy - y) ** 2) <= 30**2) & ( ((xx - x) ** 2 + (yy - y) ** 2) > 27**2 ) frame[ring] = border return frame def _make_step( nodes: dict[str, tuple[int, int]], edges: list[tuple[str, str, int]], distances: dict[str, str], current: str | None = None, visited: set[str] | None = None, active_edge: tuple[str, str] | None = None, step_text: str = "", algo_name: str = "", duration: float = STEP_DUR, ) -> CompositeVideoClip: if visited is None: visited = set() graph_frame = _render_graph(nodes, edges, distances, current, visited, active_edge) def make_frame(_t: float) -> np.ndarray: return graph_frame.copy() bg_clip = VideoClip(make_frame, duration=duration).with_fps(FPS) overlays: list[VideoClip] = [bg_clip] if algo_name: overlays.append( _tc( text=algo_name, font_size=28, color="#64B5F6", font=FONT_B, ) .with_duration(duration) .with_position((40, 20)) ) dist_items = [f"{k}: {v}" for k, v in distances.items()] table_text = "dist = { " + ", ".join(dist_items) + " }" overlays.append( _tc( text=table_text, font_size=18, color="#B0BEC5", font=FONT_R, ) .with_duration(duration) .with_position((40, 60)) ) visited_text = f"visited = {{ {', '.join(sorted(visited))} }}" overlays.append( _tc( text=visited_text, font_size=18, color="#A5D6A7", font=FONT_R, ) .with_duration(duration) .with_position((40, 90)) ) for src, dst, w in edges: sx, sy = nodes[src] dx, dy = nodes[dst] mx = (sx + dx) // 2 - 6 my = (sy + dy) // 2 - 12 wcol = "#FF8A65" if active_edge == (src, dst) else "#90A4AE" overlays.append( _tc( text=str(w), font_size=16, color=wcol, font=FONT_B, ) .with_duration(duration) .with_position((mx, my)) ) for name, (x, y) in nodes.items(): overlays.append( _tc( text=name, font_size=20, color="white", font=FONT_B, ) .with_duration(duration) .with_position((x - 7, y - 12)) ) d = distances.get(name, INF) overlays.append( _tc( text=f"d={d}", font_size=14, color="#FFE082", font=FONT_R, ) .with_duration(duration) .with_position((x - 16, y + 35)) ) if step_text: overlays.append( _tc( text=step_text, font_size=18, color="#E0E0E0", font=FONT_R, ) .with_duration(duration) .with_position((40, 600)) ) return CompositeVideoClip(overlays, size=(W, H)).with_effects( [FadeIn(0.3), FadeOut(0.3)] ) def _dijkstra_steps() -> list[CompositeVideoClip]: n = NODE_POS e = EDGES_DIJKSTRA return [ _make_step( n, e, {"S": "0", "A": INF, "B": INF, "C": INF}, current="S", step_text="Inicjalizacja: d[S]=0, reszta=∞. Wybierz S (min d).", algo_name="Algorytm Dijkstry", ), _make_step( n, e, {"S": "0", "A": "2", "B": "5", "C": INF}, current="S", active_edge=("S", "A"), step_text="Relaksacja S→A: d[A]=0+2=2. S→B: d[B]=0+5=5.", algo_name="Algorytm Dijkstry", ), _make_step( n, e, {"S": "0", "A": "2", "B": "5", "C": "5"}, current="A", visited={"S"}, active_edge=("A", "C"), step_text="Zamknij S. Min=A(2). Relaksacja A→C: d[C]=2+3=5.", algo_name="Algorytm Dijkstry", ), _make_step( n, e, {"S": "0", "A": "2", "B": "5", "C": "5"}, current="B", visited={"S", "A"}, active_edge=("B", "A"), step_text="Zamknij A. Min=B(5). B→A: 5+1=6>2, nie zmieniaj. B→C: 5+6=11>5.", algo_name="Algorytm Dijkstry", ), _make_step( n, e, {"S": "0", "A": "2", "B": "5", "C": "5"}, current="C", visited={"S", "A", "B"}, step_text="Zamknij B. Min=C(5). Koniec! Wynik: d={S:0, A:2, B:5, C:5}.", algo_name="Dijkstra -- WYNIK", ), ] def _bellman_ford_steps() -> list[CompositeVideoClip]: n = NODE_POS e = EDGES_BF return [ _make_step( n, e, {"S": "0", "A": INF, "B": INF, "C": INF}, step_text="Bellman-Ford: relaksuj WSZYSTKIE krawędzie V-1=3 razy. Ujemne wagi OK!", algo_name="Algorytm Bellmana-Forda", ), _make_step( n, e, {"S": "0", "A": "2", "B": "5", "C": "5"}, active_edge=("S", "A"), step_text="Iteracja 1: S→A:2, A→C:5, S→B:5. Potem B→A: 5+(-4)=1 < 2 → A=1!", algo_name="Bellman-Ford -- iteracja 1", ), _make_step( n, e, {"S": "0", "A": "1", "B": "5", "C": "5"}, active_edge=("B", "A"), step_text="B→A z ujemną wagą -4: d[A] poprawione z 2 na 1! (Dijkstra by to pominął!)", algo_name="Bellman-Ford -- ujemna waga", ), _make_step( n, e, {"S": "0", "A": "1", "B": "5", "C": "4"}, active_edge=("A", "C"), step_text="Iteracja 2: A→C: 1+3=4 < 5 → C=4. Propagacja poprawionego A.", algo_name="Bellman-Ford -- iteracja 2", ), _make_step( n, e, {"S": "0", "A": "1", "B": "5", "C": "4"}, step_text="Iteracja 3: brak zmian. V-ta iteracja: brak popraw → brak cyklu ujemnego.", algo_name="Bellman-Ford -- WYNIK, O(V*E)", ), ] def _astar_steps() -> list[CompositeVideoClip]: n = NODE_POS e = EDGES_DIJKSTRA return [ _make_step( n, e, {"S": "0", "A": INF, "B": INF, "C": INF}, current="S", step_text="A*: f(n)=g(n)+h(n). Cel=C. h(S)=5, h(A)=3, h(B)=4, h(C)=0. f(S)=0+5=5.", algo_name="Algorytm A*", ), _make_step( n, e, {"S": "0", "A": "2", "B": "5", "C": INF}, current="S", active_edge=("S", "A"), step_text="Relaksuj S: A(g=2,f=2+3=5), B(g=5,f=5+4=9). Min f → A(5).", algo_name="A* -- rozwijanie S", ), _make_step( n, e, {"S": "0", "A": "2", "B": "5", "C": "5"}, current="A", visited={"S"}, active_edge=("A", "C"), step_text="Rozwiń A(f=5): A→C: g=2+3=5, f=5+0=5. Min f → C(5) = CEL!", algo_name="A* -- rozwijanie A", ), _make_step( n, e, {"S": "0", "A": "2", "B": "5", "C": "5"}, current="C", visited={"S", "A"}, step_text="Dotarliśmy do C! Koszt=5. A* NIE przetwarza B (3 vs 4 w Dijkstrze).", algo_name="A* -- cel osiągnięty!", ), ] def _comparison_slide() -> CompositeVideoClip: bg = ColorClip(size=(W, H), color=BG).with_duration(12.0) title = ( _tc( text="Porównanie algorytmów", font_size=40, color="white", font=FONT_B, ) .with_duration(12.0) .with_position(("center", 40)) ) rows = [ ("Cecha", "Dijkstra", "Bellman-Ford", "A*"), ("Typ", "Zachłanny", "Prog. dynamiczne", "Heurystyczny"), ("Problem", "SSSP", "SSSP", "Single-pair"), ("Ujemne wagi", "NIE", "TAK", "NIE"), ("Cykl ujemny", "NIE wykrywa", "TAK wykrywa", "NIE"), ("Złożoność", "O((V+E)log V)", "O(V*E)", "Zależy od h(n)"), ] clips: list[VideoClip] = [bg, title] for i, row in enumerate(rows): y_pos = 120 + i * 85 for j, cell in enumerate(row): x_pos = 60 + j * 300 fs = 18 if i > 0 else 22 color = "#64B5F6" if i == 0 else "#CFD8DC" tc = ( _tc( text=cell, font_size=fs, color=color, font=FONT_B if i == 0 else FONT_R, ) .with_duration(12.0) .with_position((x_pos, y_pos)) ) clips.append(tc) return CompositeVideoClip(clips, size=(W, H)).with_effects( [FadeIn(0.5), FadeOut(0.5)] ) def main() -> None: """Generate the Q02 shortest path visualization video.""" sections: list[VideoClip] = [] sections.append( _make_header( "Pytanie 2: Algorytmy najkrótszej ścieżki", "Dijkstra * Bellman-Ford * A*", duration=8.0, ) ) sections.append(_make_header("Algorytm Dijkstry", "Zachłanny, SSSP, wagi ≥ 0")) sections.extend(_dijkstra_steps()) sections.append( _make_header("Algorytm Bellmana-Forda", "Prog. dynamiczne, ujemne wagi, O(V·E)") ) sections.extend(_bellman_ford_steps()) sections.append( _make_header("Algorytm A*", "Heurystyczny, f(n)=g(n)+h(n), Single-pair") ) sections.extend(_astar_steps()) sections.append(_comparison_slide()) sections.append( _make_header( "Podsumowanie", "Dijkstra=chciwy | Bellman-Ford=brute force x(V-1) | A*=Dijkstra+GPS", duration=8.0, ) ) final = concatenate_videoclips(sections, method="compose") final.write_videofile( OUTPUT, fps=FPS, codec="libx264", audio=False, preset="medium", threads=4 ) print(f"Video saved to: {OUTPUT}") if __name__ == "__main__": main()