Przejdź do treści

Geometria Myśli


🖥️ Stanowisko Ekspozycyjne: Geometria Myśli.

Od Zapisów Łukasiewicza do Przestrzeni AI

1. Co przedstawia animacja? (Przewodnik dla Widza)

Prezentowana animacja w sposób ciągły wizualizuje ewolucję ludzkiej myśli logicznej w matematyczną strukturę, z której korzysta współczesna sztuczna inteligencja. Projekt składa się z trzech płynnie przechodzących w siebie faz:

  • Faza 1: Architektura Języka (Drzewo Składniowe). Widzimy geometryczną strukturę klasycznego zdania logicznego. Punkty (reprezentujące cząstki informacji) pulsują na gałęziach grafu, pokazując, jak człowiek buduje hierarchię myśli. Następnie całe drzewo "spływa" na dół, układając się w idealnie płaski, liniowy ciąg znaków – to unikalny Zapis Polski.
  • Faza 2: Rozpad Topologiczny (Transformacja Chaosu). Struktura języka rozpada się. Tysiące punktów danych wylatują w przestrzeń trójwymiarową, wirując w pozornym chaosie. To moment przejścia od sztywnej logiki do ciągłej matematyki przestrzennej.
  • Faza 3: Świt Zrozumienia (Klastry Semantyczne AI). Punkty pod wpływem matematycznych przyciągań formują trzy gęste, plastyczne chmury pojęciowe (tzw. przestrzenie latentne). Sztuczna inteligencja nie rozumie słów jak człowiek – dla niej "zrozumienie" to geometryczna odległość między klastrami, co badał jeden z ojców Warszawskiej Szkoły, Kazimierz Kuratowski.

2. Wyjaśnienie Badania: O co chodzi w wyrażeniu ((p -> q) v ~r)?

Dla nie-matematyka ten ciąg znaków wygląda jak tajemne zaklęcie. W rzeczywistości to proste zdanie złożone, jakich używamy w codziennym życiu. Litery \(p, q, r\) to pojedyncze stwierdzenia (zmienne zdaniowe), a symbole to łączniki między nimi:

  • \(\rightarrow\) oznacza implikację ("jeśli..., to...")
  • \(\lor\) (lub litera v) oznacza alternatywę ("lub")
  • \(\sim\) oznacza negację ("nie-", zaprzeczenie)

Przykład z życia codziennego:

  • Niech \(p\) = "Pada deszcz"
  • Niech \(q\) = "Asfalt jest mokry"
  • Niech \(r\) = "Idę na spacer"

Nasze badane wyrażenie ((p -> q) v ~r) po ludzku brzmi następująco:

"Zachodzi reguła, że jeśli pada deszcz, to asfalt jest mokry, LUB nie idę na spacer."

Przedwojenni logicy z Warszawy zauważyli, że dla ludzkiego oka nawiasy są jasne, ale dla maszyn obliczeniowych parsowanie (analizowanie) nawiasów jest koszmarnie skomplikowane i zużywa mnóstwo pamięci.


3. Czym jest Zapis Polski i RPN (Odwrócona Notacja Polska)?

Aby wyeliminować nawiasy, profesor Jan Łukasiewicz dokonał w latach 20. XX wieku genialnego odkrycia: udowodnił, że jeśli umieścimy operatory w ściśle określonym porządku względem liter, nawiasy staną się całkowicie zbędne, a zapis zachowa absolutną jednoznaczność.
Istnieją dwie odmiany tego geniuszu:

Wariant A: Notacja Przedrostkowa (Oryginalny Zapis Łukasiewicza)

Operatory stawiamy PRZED argumentami. Nasze zdanie o deszczu i spacerze zapisujemy bez jednego nawiasu jako:

\[\lor\ \rightarrow\ p\ q\ \sim\ r\]

Komputer (lub model AI) czyta ten ciąg od lewej do prawej. Widzi operator \(\lor\) (lub), więc wie, że musi połączyć dwa duże bloki, które zaraz nastąpią. Ta metoda jest stosowana wewnątrz dzisiejszych modeli językowych (LLM) do przetwarzania tekstu.

Wariant B: Notacja Postfiksowa (RPN - Odwrócona Notacja Polska)

Lata później zachodni inżynierowie (m.in. Charles Hamblin i firma Hewlett-Packard) odwrócili pomysł Łukasiewicza, tworząc RPN, gdzie operatory stawia się ZA argumentami:

\[p\ q\ \rightarrow\ r\ \sim\ \lor\]

To właśnie ten system stał się fundamentem informatyki. Działa on na zasadzie tzw. stosu pamięci:

  1. Wrzuć do pamięci \(p\), wrzuć \(q\).
  2. Zobaczysz \(\rightarrow\)? Pobierz \(p\) oraz \(q\), wykonaj operację "jeśli... to..." i odłóż wynik na stos.
  3. Wrzuć do pamięci \(r\).
  4. Zobaczysz \(\sim\)? Zaneguj \(r\).
  5. Na końcu widzisz \(\lor\)? Weź oba przetworzone wyniki i połącz je słowem "lub".

Dzięki Polakom maszyny liczące zyskały zdolność błyskawicznego, linearnego przetwarzania skomplikowanych ludzkich myśli. Bez Łukasiewicza i bez RPN kompilatory i interpretery współczesnych języków programowania po prostu by nie istniały.


Python
"""
Uniwersalny Silnik Animacji v11 (Natywny dla X11/Qt5Cairo/Blitting)
Moduł: Warszawa AI Topology - Logika Łukasiewicza i Przestrzenie Kuratowskiego
Licencja: CC-BY-SA 4.0 / GPLv3
Wersja: 5.0 (Dostępna dla niedowidzących barw, wydłużony cykl 15s, podział na bloki)
"""
import matplotlib
matplotlib.use('Qt5Cairo') 
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
import qrcode

CONFIG = {
    "MODE": "SHOW",             # "SHOW" (okno rzutnika) lub "GIF" / "MP4" (eksport)
    "WINDOW_SIZE": 8,           
    "FPS": 30,                  
    "DARK_MODE": True,          
    "FILE": "warszawa_ai_topology.gif",

    "TITLE": "W A R S Z A W S K A   S Z K O Ł A   M A T E M A T Y C Z N A   A   A I",
    "SUBTITLE": "Od Logiki Łukasiewicza do Topologicznych Przestrzeni Latentnych Kuratowskiego",
    "SUBTITLE_COLOR": "#56B4E9",
    "QR_LINK": "https://phinix.org",
    "QR_SIZE": 0.11,
}

PHYSICS_CONFIG = {
    "SPEED": 0.50,              
    "NUM_CYCLES": 3,            # 3 pełne pętle dla idealnego zamknięcia obrotu 3D
    "NUM_POINTS": 300, # 3000,         # Liczba punktów budujących chmurę semantyczną
}

# Paleta bezpieczna dla osób z daltonizmem (Okabe-Ito)
COLOR_ORANGE  = "#E69F00"
COLOR_SKYBLUE = "#56B4E9"
COLOR_YELLOW  = "#F0E442"
COLOR_GREEN   = "#009E73"
COLOR_PURPLE  = "#CC79A7"

# Kontrastowe kolory fazy finałowej (Faza 3) dla 3 odrębnych klastrów pojęciowych
CLUSTER_COLORS = ["#56B4E9", "#E69F00", "#ffffff"] 

BG_COLOR = "#0b0c10" if CONFIG["DARK_MODE"] else "white"
FG_COLOR = "white" if CONFIG["DARK_MODE"] else "black"


def generate_qr(link, size=100):
    if not link: return None
    qr = qrcode.QRCode(box_size=2, border=1)
    qr.add_data(link)
    qr.make(fit=True)
    img = qr.make_image(fill_color="black", back_color="white")
    img = img.resize((size, size))
    return np.array(img.convert("L")) / 255.0


class WarszawaAITopologyLogic:
    """Obliczenia ewolucji od grafu drzewa logicznego do klastrów semantycznych AI."""
    def __init__(self):
        self.style = {"color": "#1f2833", "lw": 1.0, "alpha": 0.15}

    def get_style(self, order=1):
        return self.style

    def precompute_data(self):
        frames_per_cycle = 450  # 15 sekund przy 30 FPS
        total_frames = frames_per_cycle * PHYSICS_CONFIG["NUM_CYCLES"]

        level_points_data = {}
        frame_metadata = []
        n_pts = PHYSICS_CONFIG["NUM_POINTS"]

        # 1. Definicja stałych pozycji węzłów drzewa wyrażenia ((p -> q) v ~r)
        # Pozycje (X, Y) w przestrzeni znormalizowanej [0, 1]
        node_positions = np.array([
            [0.50, 0.75],  # 0: Operator główny 'v' (Alternatywa)
            [0.35, 0.60],  # 1: Podoperator '->' (Implikacja)
            [0.65, 0.60],  # 2: Podoperator '~' (Negacja)
            [0.25, 0.45],  # 3: Zmienna 'p'
            [0.45, 0.45],  # 4: Zmienna 'q'
            [0.65, 0.45]   # 5: Zmienna 'r'
        ], dtype=np.float32)

        # Pozycje liniowe na stosie Notacji Łukasiewicza (ONP / Zapis Polski) na dole ekranu
        stack_positions = np.array([
            [0.25, 0.20],  # 'v'
            [0.35, 0.20],  # '->'
            [0.45, 0.20],  # 'p'
            [0.55, 0.20],  # 'q'
            [0.65, 0.20],  # '~'
            [0.75, 0.20]   # 'r'
        ], dtype=np.float32)

        # Mapowanie punktów chmury do poszczególnych węzłów drzewa w Fazie 1
        pts_per_node = n_pts // 6
        node_assignment = np.zeros(n_pts, dtype=np.int32)
        for idx in range(6):
            node_assignment[idx * pts_per_node : (idx + 1) * pts_per_node] = idx

        # 2. Inicjalizacja stabilnej chmury punktów dla klastrów latentnych (Faza 3)
        np.random.seed(88)
        cluster_assignment = np.random.choice([0, 1, 2], size=n_pts, p=[0.35, 0.40, 0.25])

        # Centra trzech klastrów pojęciowych AI w przestrzeni sferycznej
        c_centers = np.array([[0.30, 0.50, 0.0], [0.70, 0.50, 0.0], [0.50, 0.40, 0.1]], dtype=np.float32)

        # Generowanie losowych przesunięć wewnątrz klastrów
        r_offset = np.random.normal(0, 0.04, size=(n_pts, 3)).astype(np.float32)

        dt = 1.0 / CONFIG["FPS"]

        for f in range(total_frames):
            t_sec = f * dt * PHYSICS_CONFIG["SPEED"]
            cycle_phase = f % 450

            # Globalna rotacja 3D używana w fazie chmury pojęciowej
            rot_y = t_sec * 0.8
            rot_z = t_sec * 0.5

            px = np.zeros(n_pts, dtype=np.float32)
            py = np.zeros(n_pts, dtype=np.float32)
            pz = np.zeros(n_pts, dtype=np.float32)

            # --- MASZYNA STANÓW: TRZY FAZY SZKOŁY WARSZAWSKIEJ ---
            if cycle_phase < 120:
                # FAZA 1: DRZEWO LOGICZNE I NOTACJA ŁUKASIEWICZA (0s - 4s)
                status = "STAN_LOGIKI"
                p_raw = (cycle_phase / 120) # * 100.0
                progress = (p_raw - 0.5) * 2
                # progress = 0.0
                # Płynne przejście i liniowy ruch tokenów w dół na stos w klatkach 60-120
                p_move = 0.0 if cycle_phase < 60 else (cycle_phase - 60) / 60.0
                p_interp = (1.0 - np.cos(p_move * np.pi)) / 2.0

                alpha_wire = 1.0 - (p_interp * 0.5)

                for i in range(n_pts):
                    n_idx = node_assignment[i]
                    # Lokalny szum wokół węzła grafu, tworzący efekt pulsujących skupisk
                    local_noise_x = r_offset[i, 0] * 0.4
                    local_noise_y = r_offset[i, 1] * 0.4

                    pos_start = node_positions[n_idx]
                    pos_end = stack_positions[n_idx]

                    px[i] = (1.0 - p_interp) * (pos_start[0] + local_noise_x) + p_interp * (pos_end[0] + local_noise_x)
                    py[i] = (1.0 - p_interp) * (pos_start[1] + local_noise_y) + p_interp * (pos_end[1] + local_noise_y)
                    pz[i] = r_offset[i, 2] * 0.2

                # Linie strukturalne grafu drzewa logicznego
                wire_x = [0.50, 0.35, np.nan, 0.50, 0.65, np.nan, 0.35, 0.25, np.nan, 0.35, 0.45, np.nan, 0.65, 0.65]
                wire_y = [0.75, 0.60, np.nan, 0.75, 0.60, np.nan, 0.60, 0.45, np.nan, 0.60, 0.45, np.nan, 0.60, 0.45]

            elif cycle_phase < 320:
                # FAZA 2: ROZPAD TOPOLOGICZNY I TRANSFORMA CHAOSU (4s - 10.6s)
                status = "ROZPAD_TOPOLOGICZNY"
                p_raw = (cycle_phase - 120) / 200.0
                progress = (1.0 - np.cos(p_raw * np.pi)) / 2.0

                # Całkowite wygaszenie linii grafu
                alpha_wire = np.clip(1.0 - p_raw * 3.0, 0.0, 1.0)

                for i in range(n_pts):
                    n_idx = node_assignment[i]
                    c_idx = cluster_assignment[i]

                    # Pozycja startowa (koniec Fazy 1 - stos ONP)
                    pos_start_x = stack_positions[n_idx, 0] + r_offset[i, 0] * 0.4
                    pos_start_y = stack_positions[n_idx, 1] + r_offset[i, 1] * 0.4
                    pos_start_z = r_offset[i, 2] * 0.2

                    # Pozycja docelowa w trójwymiarowym klastrze Kuratowskiego
                    pos_end_x = c_centers[c_idx, 0] + r_offset[i, 0]
                    pos_end_y = c_centers[c_idx, 1] + r_offset[i, 1]
                    pos_end_z = c_centers[c_idx, 2] + r_offset[i, 2]

                    # Rotacja matematyczna układu współrzędnych w trakcie lotu (efekt 3D)
                    rx = pos_end_x - c_centers[c_idx, 0]
                    ry = pos_end_y - c_centers[c_idx, 1]
                    rz = pos_end_z - c_centers[c_idx, 2]

                    rot_x_val = rx * np.cos(rot_y) - rz * np.sin(rot_y) + c_centers[c_idx, 0]
                    rot_y_val = ry
                    rot_z_val = rx * np.sin(rot_y) + rz * np.cos(rot_y) + c_centers[c_idx, 2]

                    px[i] = (1.0 - progress) * pos_start_x + progress * rot_x_val
                    py[i] = (1.0 - progress) * pos_start_y + progress * rot_y_val
                    pz[i] = (1.0 - progress) * pos_start_z + progress * rot_z_val

                wire_x, wire_y = [0.0], [0.0]

            else:
                # FAZA 3: MAPOWANIE SEMANTYCZNE / EMBEDDINGS (10.6s - 15s)
                status = "MAPOWANIE_SEMANTYCZNE"
                progress = 1.0
                alpha_wire = 0.0

                for i in range(n_pts):
                    c_idx = cluster_assignment[i]

                    base_x_val = c_centers[c_idx, 0] + r_offset[i, 0]
                    base_y_val = c_centers[c_idx, 1] + r_offset[i, 1]
                    base_z_val = c_centers[c_idx, 2] + r_offset[i, 2]

                    # Pełna i stabilna rotacja wokół osi Y i Z dla kinowego efektu głębi
                    rx = base_x_val - c_centers[c_idx, 0]
                    ry = base_y_val - c_centers[c_idx, 1]
                    rz = base_z_val - c_centers[c_idx, 2]

                    x_r = rx * np.cos(rot_y) - rz * np.sin(rot_y)
                    z_r = rx * np.sin(rot_y) + rz * np.cos(rot_y)

                    px[i] = x_r + c_centers[c_idx, 0]
                    py[i] = ry * np.cos(rot_z) - z_r * np.sin(rot_z) + c_centers[c_idx, 1]
                    pz[i] = ry * np.sin(rot_z) + z_r * np.cos(rot_z) + c_centers[c_idx, 2]

                wire_x, wire_y = [0.0], [0.0]

            # RĘCZNY Z-BUFFERING: Sortowanie tablic względem osi głębi Z
            sort_indices = np.argsort(pz)

            level_points_data[f] = {
                'x': px[sort_indices],
                'y': py[sort_indices],
                'z': pz[sort_indices],
                'node_ids': node_assignment[sort_indices],
                'cluster_ids': cluster_assignment[sort_indices],
                'wire_x': np.array(wire_x, dtype=np.float32),
                'wire_y': np.array(wire_y, dtype=np.float32),
                'wire_alpha': float(alpha_wire)
            }
            frame_metadata.append((status, progress, f))

        return level_points_data, frame_metadata

class AnimationEngine:
    def __init__(self, logic_class):
        self.logic = logic_class()
        plt.style.use('dark_background' if CONFIG["DARK_MODE"] else 'default')

        # Tworzenie okna prezentacyjnego 14x7
        fig_size = (CONFIG["WINDOW_SIZE"], CONFIG["WINDOW_SIZE"]) if CONFIG["MODE"] == "SHOW" else (4, 4)
        self.fig, self.ax = plt.subplots(figsize=fig_size)

        # self.fig, self.ax = plt.subplots(figsize=(14, 7) if CONFIG["MODE"] == "SHOW" else (5, 3))
        self.fig.patch.set_facecolor(BG_COLOR)
        self.ax.set_facecolor(BG_COLOR)
        self.ax.axis('off')
        self.ax.set_xlim(-0.02, 1.02)
        self.ax.set_ylim(-0.02, 1.02)

        # t_size = 11 if CONFIG["MODE"] == "SHOW" else 7
        # st_size = 7.5 if CONFIG["MODE"] == "SHOW" else 5.5
        t_size = 11 if CONFIG["MODE"] == "SHOW" else 7
        st_size = 10 if CONFIG["MODE"] == "SHOW" else 5.5
        stat_size = 9 if CONFIG["MODE"] == "SHOW" else 5.5
        math_size = 8 if CONFIG["MODE"] == "SHOW" else 5
        st2_size = 7.5 if CONFIG["MODE"] == "SHOW" else 4

        # Nagłówki i Filar Warszawski ekspozycji
        self.ax.text(0.5, 1.05, CONFIG["TITLE"], color=FG_COLOR, fontsize=t_size, fontweight='bold', ha='center', va='bottom', transform=self.ax.transAxes)
        self.ax.text(0.5, 1.01, CONFIG["SUBTITLE"], color=CONFIG["SUBTITLE_COLOR"], fontsize=st_size, ha='center', va='bottom', alpha=0.7, transform=self.ax.transAxes)

        self.ax.text(0.02, 0.88, "FILAR WARSZAWSKI: LOGIKA I TEORIA MODELI\nPrecyzja sformalizowanych struktur", color="#888888", fontsize=st2_size, fontweight='bold', va='center')
        self.ax.text(0.98, 0.88, "FILAR LWOWSKI: KAWIARNIA SZKOCKA\nDynamiczne operacje przestrzenne", color="#45a29e", fontsize=st2_size, fontweight='bold', va='center', ha='right', transform=self.ax.transAxes)

        # Dynamiczne teksty statusów i objaśnień na dole ekranu
        self.txt_status = self.ax.text(0.5, 0.01, "STATUS: SYSTEM INICJALIZACJI", color="#444444", fontsize=stat_size, fontweight='bold', ha='center')
        self.txt_math = self.ax.text(0.5, 0.06, "OPERATOR GRUPY: INAKTYWNY", color="#444444", fontsize=math_size, fontweight='bold', ha='center')

        # Statyczny opis wyrażenia logicznego (Faza 1)
        self.txt_expression = self.ax.text(0.5, 0.82, "Badane wyrażenie:  ((p -> q) v ~r)", color="#ffffff", fontsize=st_size+1, fontweight='bold', ha='center', animated=True)

        # Etykiety dla trzech klastrów pojęciowych AI (Faza 3)
        self.lbl_cluster_a = self.ax.text(0.30, 0.61, "", color=CLUSTER_COLORS[0], fontsize=st_size, fontweight='bold', ha='center', animated=True)
        self.lbl_cluster_b = self.ax.text(0.70, 0.61, "", color=CLUSTER_COLORS[1], fontsize=st_size, fontweight='bold', ha='center', animated=True)
        self.lbl_cluster_c = self.ax.text(0.50, 0.28, "", color=CLUSTER_COLORS[2], fontsize=st_size, fontweight='bold', ha='center', animated=True)

        # Renderowanie i nakładanie kodu QR na dole ekranu
        if CONFIG["MODE"] == "SHOW" and CONFIG["QR_LINK"]:
            qr_img = generate_qr(CONFIG["QR_LINK"], int(100 * CONFIG["WINDOW_SIZE"] / 8))
            if qr_img is not None:
                self.qr_ax = self.fig.add_axes([1 - CONFIG["QR_SIZE"] - 0.01, 0.01, CONFIG["QR_SIZE"], CONFIG["QR_SIZE"]], anchor='SE')
                self.qr_ax.imshow(qr_img, cmap='gray')
                self.qr_ax.axis('off')

        self.line_horizon, = self.ax.plot([], [], color="#1f2833", lw=1.0, alpha=0.15, zorder=1)

        # Rezerwacja obiektów rysunkowych dla silnika szybkiego blittingu
        self.scatter = self.ax.scatter([], [], s=8.0, zorder=3)
        self.wire_mesh, = self.ax.plot([], [], color="#56B4E9", lw=1.5, ls="-", alpha=1.0, zorder=4)

        print("Uruchamiam prekompilację ewolucji struktur i bufora głębokości (Z-buffer)...")
        self.level_points_data, self.frame_metadata = self.logic.precompute_data()
        self.draw_frames_count = len(self.frame_metadata)
        print(f"Prekompilacja zakończona sukcesem. Wygenerowano {self.draw_frames_count} klatek.")

        self.fig.canvas.flush_events()
        self.ani = animation.FuncAnimation(
            self.fig, self.update, frames=self.draw_frames_count, interval=1000/CONFIG["FPS"],
            blit=True, repeat=True
        )

        if CONFIG["MODE"] == "SHOW":
            plt.show()
        else:
            self.save_gif()

    def update(self, i):
        status, progress, global_frame_idx = self.frame_metadata[i]
        data = self.level_points_data[global_frame_idx]

        # 1. Obsługa dynamicznego interfejsu tekstowego i komunikatów fazowych
        if status == "STAN_LOGIKI":
            self.txt_status.set_text("JAN ŁUKASIEWICZ: STRUKTURA DRZEWA SKŁADNIOWEGO JĘZYKA")
            self.txt_status.set_color("#ffffff")
            self.txt_status.set_alpha(0.9)

            if progress < 0.0:
                self.txt_math.set_text("LOGIKA FORMALNA: Klasyczna reprezentacja relacji logicznych")
            else:
                self.txt_math.set_text(f"LINEARYZACJA STOSU: Konwersja drzewa do Notacji Beznawiasowej (ONP): {int(progress*100)}%")
            # self.txt_math.set_text(f"LINEARYZACJA STOSU: Konwersja drzewa do Notacji Beznawiasowej (ONP): {int(progress*100)}%")

            self.txt_math.set_color("#888888")
            self.txt_expression.set_text("Wyrażenie:  ((p -> q) v ~r)   --->   Zapis Polski:  v -> p q ~ r")
            self.txt_expression.set_alpha(1.0)

            # Ukrycie etykiet klastrów AI w fazie początkowej
            self.lbl_cluster_a.set_text("")
            self.lbl_cluster_b.set_text("")
            self.lbl_cluster_c.set_text("")

        elif status == "ROZPAD_TOPOLOGICZNY":
            self.txt_status.set_text("KAZIMIERZ KURATOWSKI: DEKOMPOZYCJA STRUKTURALNA I CHAOS")
            self.txt_status.set_color(COLOR_ORANGE)
            self.txt_status.set_alpha(0.95)
            self.txt_math.set_text(f"PRZESTRZEŃ HAUSDORFFA: Rozpad dyskretnych tokenów w chmurę punktów: {int(progress*100)}%")
            self.txt_math.set_color("#ffffff")
            self.txt_expression.set_alpha(max(0.0, 1.0 - progress * 3.0))

            self.lbl_cluster_a.set_text("")
            self.lbl_cluster_b.set_text("")
            self.lbl_cluster_c.set_text("")

        else: # MAPOWANIE_SEMANTYCZNE
            self.txt_status.set_text("WSPÓŁCZESNA SZTUCZNA INTELIGENCJA: PRZESTRZEŃ LATENTNA (EMBEDDINGS)")
            self.txt_status.set_color(COLOR_GREEN)
            self.txt_status.set_alpha(0.95)
            self.txt_math.set_text("TOPOLOGIA MODELU LLM: Reprezentacja pojęć poprzez odległości geometryczne")
            self.txt_math.set_color(COLOR_GREEN)
            self.txt_expression.set_text("")

            # Włączenie etykiet semantycznych dla uformowanych klastrów AI
            self.lbl_cluster_a.set_text("KLASTER A:\nJĘZYK NATURALNY\n(Semantyka)")
            self.lbl_cluster_b.set_text("KLASTER B:\nKOD MASZYNOWY\n(Syntaktyka)")
            self.lbl_cluster_c.set_text("KLASTER C:\nLOGIKA & MATEMATYKA\n(Abstrakcja)")

        self.line_horizon.set_data([0.02, 0.98], [0.50, 0.50])

        # 2. Mapowanie bezpiecznych kolorów (Okabe-Ito vs Klastry AI) z Z-bufferingiem
        if status in ["STAN_LOGIKI", "ROZPAD_TOPOLOGICZNY"]:
            # W fazach 1 i 2 przypisujemy kolory na podstawie węzłów początkowych (Dostępność barw)
            colors_pool = np.array([COLOR_ORANGE, COLOR_SKYBLUE, COLOR_YELLOW, COLOR_GREEN, COLOR_PURPLE, "#ffffff"])
            base_colors = colors_pool[data['node_ids']]
        else:
            # W fazie 3 punkty przyjmują kolory przydzielonych im klastrów semantycznych AI
            cluster_colors_pool = np.array(CLUSTER_COLORS)
            base_colors = cluster_colors_pool[data['cluster_ids']]

        # Wyliczenie natężenia światła na podstawie odległości Z-depth (Cieniowanie głębi)
        z_min, z_max = -0.06, 0.06
        shade = 0.3 + 0.7 * (data['z'] - z_min) / (z_max - z_min + 1e-5)
        shade = np.clip(shade, 0.3, 1.0)

        from matplotlib.colors import to_rgba
        rgba_colors = np.array([to_rgba(c) for c in base_colors])
        rgba_colors[:, 3] = shade * 0.90  # Połączenie jasności z przezroczystością dla plastyki 3D

        # 3. Aktualizacja pozycji i barw w silniku blittingu ekranu
        self.scatter.set_offsets(np.column_stack((data['x'], data['y'])))
        self.scatter.set_facecolors(rgba_colors)
        self.scatter.set_edgecolors(rgba_colors)

        # 4. Aktualizacja i kontrola widoczności linii pomocniczych drzewa logicznego
        self.wire_mesh.set_data(data['wire_x'], data['wire_y'])
        self.wire_mesh.set_alpha(data['wire_alpha'])

        return [self.line_horizon, self.txt_status, self.txt_math, self.txt_expression,
                self.lbl_cluster_a, self.lbl_cluster_b, self.lbl_cluster_c, self.scatter, self.wire_mesh]

    def save_gif(self):
        print("Generuję zoptymalizowany plik GIF dla topologii Warszawskiej...")
        writer = animation.PillowWriter(fps=CONFIG["FPS"])
        self.ani.save(CONFIG["FILE"], writer=writer, dpi=100)
        print("Zapisano pomyślnie: ", CONFIG["FILE"])
        plt.close()

    def save_mp4(self):
        print("Generuję plik MP4 (H.264) dla topologii Warszawskiej...")
        writer = animation.FFMpegWriter(
            fps=CONFIG["FPS"], 
            codec="libx264", 
            extra_args=['-pix_fmt', 'yuv420p', '-crf', '18']
        )
        self.ani.save("warszawa_ai_topology.mp4", writer=writer, dpi=150)
        print("Zapisano pomyślnie plik MP4!")
        plt.close()


if __name__ == "__main__":
    AnimationEngine(WarszawaAITopologyLogic)