Spaces:
Running
Running
| """ | |
| Attractor Swarm Node | |
| ===================== | |
| "Not one eye looking at the field - many eyes looking at each other AND the field." | |
| This implements the full theory developed by Antti, Claude, ChatGPT, and Gemini: | |
| 1. SINGLE ATTRACTOR + OPTIC: | |
| - An attractor observes the field through a coupling kernel K_κ | |
| - Coupling = integration window width = spectral bandwidth | |
| - F_eff(t) = (S * K_κ)(t) | |
| 2. MULTIPLE ATTRACTORS + INTER-OPTICS: | |
| - Each attractor i has its own field coupling κ_i | |
| - Each attractor i observes other attractors j through inter-optics κ_ij | |
| - m_ij(t) = O_κij[x_j(t)] - "how sharply does i sample j?" | |
| 3. THE THREE REGIMES: | |
| - Low κ: over-integration → soup (structure dies) | |
| - Critical κ: balanced → lattices/stars (maximal structure) | |
| - High κ: bandwidth > Nyquist → stripes (aliasing collapse) | |
| 4. COALITION FORMATION: | |
| - When κ_ij rises between a subset, they share high-detail info | |
| - They lock phases → form "super-attractor" coalitions | |
| - This is attention implemented as signal theory | |
| 5. HOMEOSTATIC CONTROL: | |
| - κ_ij adapts based on whether j helps i reduce error/increase structure | |
| - The network learns who to couple to, moment by moment | |
| CREATED: December 2025 | |
| AUTHORS: Antti + Claude + ChatGPT + Gemini | |
| """ | |
| import numpy as np | |
| import cv2 | |
| from collections import deque | |
| from scipy import signal as scipy_signal | |
| from scipy.ndimage import gaussian_filter | |
| import __main__ | |
| try: | |
| BaseNode = __main__.BaseNode | |
| QtGui = __main__.QtGui | |
| except AttributeError: | |
| from PyQt6 import QtGui | |
| class BaseNode: | |
| def __init__(self): | |
| self.inputs = {} | |
| self.outputs = {} | |
| def get_blended_input(self, name, mode): | |
| return None | |
| class AttractorSwarmNode(BaseNode): | |
| NODE_CATEGORY = "Consciousness" | |
| NODE_TITLE = "Attractor Swarm" | |
| NODE_COLOR = QtGui.QColor(255, 100, 50) # Orange - swarm intelligence | |
| def __init__(self): | |
| super().__init__() | |
| self.node_title = "Attractor Swarm (Multi-Observer Optics)" | |
| self.inputs = { | |
| 'theta_signal': 'signal', | |
| 'alpha_signal': 'signal', | |
| 'beta_signal': 'signal', | |
| 'gamma_signal': 'signal', | |
| 'token_stream': 'spectrum', | |
| 'global_coupling': 'signal', # Base coupling for all | |
| 'adaptation_rate': 'signal', # How fast optics adapt | |
| 'lattice_zoom': 'signal', | |
| 'lattice_freq': 'signal', | |
| 'reset': 'signal' | |
| } | |
| self.outputs = { | |
| 'display': 'image', | |
| 'swarm_field': 'complex_spectrum', # Combined lattice | |
| 'coupling_matrix': 'spectrum', # Who couples to whom | |
| 'coalition_labels': 'spectrum', # Which attractors form groups | |
| 'dominant_attractor': 'signal', # Which one is "winning" | |
| 'global_symmetry': 'signal', # 6-fold symmetry score | |
| 'anisotropy': 'signal', # Stripe detection | |
| 'criticality_score': 'signal', # How close to critical | |
| } | |
| # === SWARM PARAMETERS === | |
| self.N = 4 # Number of attractors (one per band initially) | |
| self.field_size = 64 | |
| self.epoch = 0 | |
| # === ATTRACTOR STATES === | |
| # Each attractor has a complex state (amplitude + phase) | |
| self.states = np.ones(self.N, dtype=np.complex128) | |
| self.state_phases = np.zeros(self.N) | |
| # === FIELD OBSERVATIONS === | |
| # What each attractor sees from the raw field | |
| self.field_obs = np.zeros(self.N, dtype=np.complex128) | |
| # === OPTICS MATRICES === | |
| # κ_i: how each attractor couples to the field | |
| self.field_kappa = np.ones(self.N) * 0.5 # Start at critical | |
| # κ_ij: how attractor i couples to attractor j (inter-optics) | |
| # This is the key new structure | |
| self.inter_kappa = np.ones((self.N, self.N)) * 0.3 | |
| np.fill_diagonal(self.inter_kappa, 0) # Don't self-couple | |
| # === INTEGRATION WINDOWS (derived from kappa) === | |
| # Higher kappa = sharper window = more high-freq detail | |
| self.integration_windows = np.ones(self.N) * 10 # samples | |
| # === HISTORIES FOR WINDOWED INTEGRATION === | |
| self.history_len = 100 | |
| self.band_histories = [deque(maxlen=self.history_len) for _ in range(4)] | |
| self.state_histories = [deque(maxlen=self.history_len) for _ in range(self.N)] | |
| # === ADAPTATION === | |
| self.adaptation_rate = 0.01 | |
| self.symmetry_target = 0.5 # Try to stay near critical | |
| # === METRICS === | |
| self.symmetry_scores = np.zeros(self.N) | |
| self.anisotropy_scores = np.zeros(self.N) | |
| self.coalition_matrix = np.zeros((self.N, self.N)) | |
| # === LATTICE PARAMETERS === | |
| self.lattice_zoom = 1.0 | |
| self.lattice_freq = 4.0 | |
| # === FIELDS === | |
| self.individual_fields = [np.zeros((self.field_size, self.field_size), dtype=np.complex128) | |
| for _ in range(self.N)] | |
| self.combined_field = np.zeros((self.field_size, self.field_size), dtype=np.complex128) | |
| # === DISPLAY === | |
| self._display = np.zeros((700, 1000, 3), dtype=np.uint8) | |
| # === LABELS === | |
| self.attractor_names = ['θ-Slow', 'α-Mid', 'β-Fast', 'γ-Ultra'] | |
| self.attractor_colors = [(100, 150, 255), (100, 255, 150), (255, 200, 100), (255, 100, 150)] | |
| def _parse_input(self, val): | |
| """Parse various input formats to float""" | |
| if val is None: | |
| return 0.0 | |
| if isinstance(val, (int, float, np.floating)): | |
| return float(val) | |
| if isinstance(val, np.ndarray): | |
| return float(np.mean(np.abs(val))) if val.size > 0 else 0.0 | |
| if isinstance(val, (list, tuple)) and len(val) > 0: | |
| return float(val[0]) if not hasattr(val[0], '__len__') else 0.0 | |
| return 0.0 | |
| def _apply_optic(self, signal_history, kappa): | |
| """ | |
| Apply the optic kernel to a signal history. | |
| This is the core operation: convolution with Gaussian kernel | |
| whose width is controlled by kappa. | |
| High kappa = narrow window = high-freq passes | |
| Low kappa = wide window = averaging/smoothing | |
| """ | |
| if len(signal_history) < 5: | |
| return 0.0 + 0j | |
| sig = np.array(list(signal_history)) | |
| n = len(sig) | |
| # Kappa controls window sharpness | |
| # Higher kappa = sharper (smaller sigma) | |
| sigma = max(1.0, 10.0 / (kappa + 0.1)) | |
| # Create Gaussian kernel | |
| t = np.arange(n) | |
| center = n - 1 # Weight toward recent | |
| kernel = np.exp(-((t - center) ** 2) / (2 * sigma ** 2)) | |
| kernel = kernel / (kernel.sum() + 1e-10) | |
| # Apply kernel (weighted integration) | |
| integrated = np.sum(sig * kernel) | |
| # Estimate phase from recent samples | |
| if len(sig) > 10: | |
| try: | |
| analytic = scipy_signal.hilbert(sig - np.mean(sig)) | |
| phase = np.angle(analytic[-1]) | |
| amp = np.abs(integrated) | |
| return amp * np.exp(1j * phase) | |
| except: | |
| return integrated + 0j | |
| return integrated + 0j | |
| def _compute_inter_observation(self, i, j): | |
| """ | |
| Compute how attractor i observes attractor j through their inter-optic. | |
| m_ij(t) = O_κij[x_j(t)] | |
| """ | |
| if len(self.state_histories[j]) < 5: | |
| return 0.0 + 0j | |
| kappa_ij = self.inter_kappa[i, j] | |
| # Get j's state history as real values for integration | |
| j_history = [np.abs(s) for s in self.state_histories[j]] | |
| return self._apply_optic(j_history, kappa_ij) | |
| def _create_attractor_field(self, attractor_idx): | |
| """ | |
| Create the lattice field for one attractor based on its state | |
| and its coupling to others. | |
| """ | |
| size = self.field_size | |
| span = np.pi * self.lattice_zoom | |
| x = np.linspace(-span, span, size) | |
| y = np.linspace(-span, span, size) | |
| X, Y = np.meshgrid(x, y) | |
| field = np.zeros((size, size), dtype=np.complex128) | |
| # This attractor's state | |
| state = self.states[attractor_idx] | |
| amp = np.abs(state) | |
| phase = np.angle(state) | |
| # Base frequency modulated by attractor index | |
| base_freq = self.lattice_freq * (1 + attractor_idx * 0.2) | |
| # 6 waves at 60° for hexagonal lattice | |
| for i in range(6): | |
| angle = i * np.pi / 3 + phase | |
| # Modulate amplitude by inter-coupling | |
| # Attractors that are strongly coupled contribute more | |
| coupling_boost = 1.0 | |
| for j in range(self.N): | |
| if j != attractor_idx: | |
| coupling_boost += 0.2 * self.inter_kappa[attractor_idx, j] * np.abs(self.states[j]) | |
| wave_amp = amp * coupling_boost / self.N | |
| kx = base_freq * np.cos(angle) | |
| ky = base_freq * np.sin(angle) | |
| wave = wave_amp * np.exp(1j * (kx * X + ky * Y)) | |
| field += wave | |
| return field | |
| def _compute_symmetry_score(self, field): | |
| """ | |
| Compute 6-fold symmetry score from field. | |
| High score = hexagonal/star pattern (critical regime) | |
| Low score = stripes or soup | |
| """ | |
| # FFT of magnitude | |
| mag = np.abs(field) | |
| fft = np.fft.fftshift(np.fft.fft2(mag)) | |
| power = np.abs(fft) ** 2 | |
| # Sample at 60° intervals around center | |
| center = self.field_size // 2 | |
| radius = self.field_size // 4 | |
| angles = np.arange(0, 360, 60) * np.pi / 180 | |
| samples = [] | |
| for angle in angles: | |
| x = int(center + radius * np.cos(angle)) | |
| y = int(center + radius * np.sin(angle)) | |
| if 0 <= x < self.field_size and 0 <= y < self.field_size: | |
| samples.append(power[y, x]) | |
| if len(samples) < 6: | |
| return 0.0 | |
| samples = np.array(samples) | |
| # High symmetry = all samples similar | |
| mean_power = np.mean(samples) | |
| if mean_power < 1e-10: | |
| return 0.0 | |
| std_power = np.std(samples) | |
| symmetry = 1.0 - (std_power / (mean_power + 1e-10)) | |
| return max(0, min(1, symmetry)) | |
| def _compute_anisotropy(self, field): | |
| """ | |
| Compute anisotropy (stripe-ness) of field. | |
| High anisotropy = collapsed into stripes (over-coupled) | |
| Low anisotropy = isotropic (soup or lattice) | |
| """ | |
| mag = np.abs(field) | |
| # Compute directional gradients | |
| gx = np.abs(np.diff(mag, axis=1)).mean() | |
| gy = np.abs(np.diff(mag, axis=0)).mean() | |
| # Anisotropy = difference between directional gradients | |
| total = gx + gy + 1e-10 | |
| anisotropy = abs(gx - gy) / total | |
| return anisotropy | |
| def _detect_coalitions(self): | |
| """ | |
| Detect which attractors form coalitions based on: | |
| - High inter-coupling | |
| - Phase synchrony | |
| - Similar symmetry scores | |
| """ | |
| coalition = np.zeros((self.N, self.N)) | |
| for i in range(self.N): | |
| for j in range(i + 1, self.N): | |
| # Coupling strength | |
| coupling = (self.inter_kappa[i, j] + self.inter_kappa[j, i]) / 2 | |
| # Phase coherence | |
| phase_diff = np.abs(np.angle(self.states[i]) - np.angle(self.states[j])) | |
| phase_coherence = np.cos(phase_diff) | |
| # Symmetry similarity | |
| sym_diff = np.abs(self.symmetry_scores[i] - self.symmetry_scores[j]) | |
| sym_similarity = 1.0 - sym_diff | |
| # Coalition score | |
| score = coupling * (0.5 + 0.3 * phase_coherence + 0.2 * sym_similarity) | |
| coalition[i, j] = score | |
| coalition[j, i] = score | |
| self.coalition_matrix = coalition | |
| return coalition | |
| def _adapt_optics(self): | |
| """ | |
| Homeostatic adaptation of optics. | |
| Rule: if attractor j helps attractor i maintain critical regime, | |
| increase κ_ij. Otherwise decrease. | |
| """ | |
| for i in range(self.N): | |
| # Target: maximize symmetry, minimize anisotropy | |
| reward_i = self.symmetry_scores[i] - self.anisotropy_scores[i] | |
| # Field coupling: try to stay near critical | |
| error = self.symmetry_target - self.symmetry_scores[i] | |
| self.field_kappa[i] += self.adaptation_rate * error | |
| self.field_kappa[i] = np.clip(self.field_kappa[i], 0.1, 2.0) | |
| # Inter-coupling: strengthen connections that help | |
| for j in range(self.N): | |
| if i == j: | |
| continue | |
| # How much does j's influence correlate with i's reward? | |
| j_influence = np.abs(self.states[j]) * self.inter_kappa[i, j] | |
| # Simple rule: if both have good symmetry, strengthen | |
| j_reward = self.symmetry_scores[j] - self.anisotropy_scores[j] | |
| combined = reward_i * j_reward | |
| self.inter_kappa[i, j] += self.adaptation_rate * combined | |
| self.inter_kappa[i, j] = np.clip(self.inter_kappa[i, j], 0.05, 1.0) | |
| def step(self): | |
| self.epoch += 1 | |
| # === GET INPUTS === | |
| theta = self._parse_input(self.get_blended_input('theta_signal', 'sum')) | |
| alpha = self._parse_input(self.get_blended_input('alpha_signal', 'sum')) | |
| beta = self._parse_input(self.get_blended_input('beta_signal', 'sum')) | |
| gamma = self._parse_input(self.get_blended_input('gamma_signal', 'sum')) | |
| global_coupling = self._parse_input(self.get_blended_input('global_coupling', 'sum')) | |
| adaptation = self._parse_input(self.get_blended_input('adaptation_rate', 'sum')) | |
| zoom = self._parse_input(self.get_blended_input('lattice_zoom', 'sum')) | |
| freq = self._parse_input(self.get_blended_input('lattice_freq', 'sum')) | |
| reset = self._parse_input(self.get_blended_input('reset', 'sum')) | |
| token_stream = self.get_blended_input('token_stream', 'sum') | |
| # Handle reset | |
| if reset > 0.5: | |
| self.states = np.ones(self.N, dtype=np.complex128) | |
| self.inter_kappa = np.ones((self.N, self.N)) * 0.3 | |
| np.fill_diagonal(self.inter_kappa, 0) | |
| self.field_kappa = np.ones(self.N) * 0.5 | |
| return | |
| # Update parameters | |
| if global_coupling > 0: | |
| self.field_kappa[:] = global_coupling | |
| if adaptation > 0: | |
| self.adaptation_rate = adaptation | |
| if zoom > 0: | |
| self.lattice_zoom = np.clip(zoom, 0.25, 8.0) | |
| if freq > 0: | |
| self.lattice_freq = np.clip(freq, 1.0, 16.0) | |
| # Extract from token stream | |
| if token_stream is not None: | |
| try: | |
| if isinstance(token_stream, np.ndarray) and len(token_stream) >= 4: | |
| theta = float(token_stream[0]) if theta == 0 else theta | |
| alpha = float(token_stream[1]) if alpha == 0 else alpha | |
| beta = float(token_stream[2]) if beta == 0 else beta | |
| gamma = float(token_stream[3]) if gamma == 0 else gamma | |
| except: | |
| pass | |
| # === UPDATE HISTORIES === | |
| bands = [theta, alpha, beta, gamma] | |
| for i, b in enumerate(bands): | |
| self.band_histories[i].append(b) | |
| # === FIELD OBSERVATIONS === | |
| # Each attractor observes the field through its optic | |
| for i in range(self.N): | |
| self.field_obs[i] = self._apply_optic(self.band_histories[i], self.field_kappa[i]) | |
| # === INTER-ATTRACTOR OBSERVATIONS === | |
| inter_obs = np.zeros((self.N, self.N), dtype=np.complex128) | |
| for i in range(self.N): | |
| for j in range(self.N): | |
| if i != j: | |
| inter_obs[i, j] = self._compute_inter_observation(i, j) | |
| # === STATE UPDATE === | |
| # Each attractor's state is pulled by: | |
| # 1. Its field observation | |
| # 2. Its observations of other attractors | |
| eta_field = 0.3 # Field coupling strength | |
| eta_inter = 0.2 # Inter-attractor coupling strength | |
| for i in range(self.N): | |
| # Field pull | |
| field_pull = eta_field * self.field_kappa[i] * self.field_obs[i] | |
| # Inter-attractor pull | |
| inter_pull = 0j | |
| for j in range(self.N): | |
| if i != j: | |
| inter_pull += eta_inter * self.inter_kappa[i, j] * inter_obs[i, j] | |
| # Update state | |
| self.states[i] = (1 - eta_field - eta_inter) * self.states[i] + field_pull + inter_pull | |
| # Normalize to prevent blowup | |
| if np.abs(self.states[i]) > 10: | |
| self.states[i] = self.states[i] / np.abs(self.states[i]) * 10 | |
| # === RECORD STATE HISTORIES === | |
| for i in range(self.N): | |
| self.state_histories[i].append(self.states[i]) | |
| # === CREATE INDIVIDUAL FIELDS === | |
| for i in range(self.N): | |
| self.individual_fields[i] = self._create_attractor_field(i) | |
| # === COMPUTE METRICS === | |
| for i in range(self.N): | |
| self.symmetry_scores[i] = self._compute_symmetry_score(self.individual_fields[i]) | |
| self.anisotropy_scores[i] = self._compute_anisotropy(self.individual_fields[i]) | |
| # === COMBINE FIELDS === | |
| # Weighted by symmetry score - better observers contribute more | |
| self.combined_field = np.zeros((self.field_size, self.field_size), dtype=np.complex128) | |
| total_weight = 0 | |
| for i in range(self.N): | |
| weight = self.symmetry_scores[i] + 0.1 # Avoid zero weight | |
| self.combined_field += weight * self.individual_fields[i] | |
| total_weight += weight | |
| self.combined_field /= (total_weight + 1e-10) | |
| # === DETECT COALITIONS === | |
| self._detect_coalitions() | |
| # === ADAPT OPTICS === | |
| if self.adaptation_rate > 0: | |
| self._adapt_optics() | |
| # === UPDATE DISPLAY === | |
| self._update_display() | |
| def _update_display(self): | |
| """Create comprehensive visualization""" | |
| img = np.zeros((700, 1000, 3), dtype=np.uint8) | |
| img[:] = (20, 25, 30) | |
| # === TITLE === | |
| cv2.putText(img, "ATTRACTOR SWARM - Multi-Observer Optics", (20, 30), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 150, 50), 2) | |
| cv2.putText(img, f"Epoch: {self.epoch} | N={self.N} attractors", (20, 55), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.4, (150, 150, 200), 1) | |
| # === INDIVIDUAL ATTRACTOR PANELS === | |
| panel_size = 100 | |
| panel_y = 80 | |
| for i in range(self.N): | |
| panel_x = 20 + i * (panel_size + 30) | |
| # Label | |
| cv2.putText(img, self.attractor_names[i], (panel_x, panel_y - 5), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.4, self.attractor_colors[i], 1) | |
| # Field visualization | |
| field = self.individual_fields[i] | |
| mag = np.abs(field) | |
| phase = np.angle(field) | |
| hsv = np.zeros((self.field_size, self.field_size, 3), dtype=np.uint8) | |
| hsv[:, :, 0] = ((phase + np.pi) / (2 * np.pi) * 180).astype(np.uint8) | |
| hsv[:, :, 1] = 200 | |
| hsv[:, :, 2] = (mag / (mag.max() + 1e-10) * 255).astype(np.uint8) | |
| field_color = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) | |
| field_resized = cv2.resize(field_color, (panel_size, panel_size)) | |
| img[panel_y:panel_y + panel_size, panel_x:panel_x + panel_size] = field_resized | |
| # Metrics | |
| cv2.putText(img, f"Sym: {self.symmetry_scores[i]:.2f}", | |
| (panel_x, panel_y + panel_size + 15), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.3, (200, 200, 200), 1) | |
| cv2.putText(img, f"Ani: {self.anisotropy_scores[i]:.2f}", | |
| (panel_x, panel_y + panel_size + 30), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.3, (200, 200, 200), 1) | |
| cv2.putText(img, f"κ: {self.field_kappa[i]:.2f}", | |
| (panel_x, panel_y + panel_size + 45), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.3, (200, 200, 200), 1) | |
| # === COMBINED FIELD === | |
| combined_x, combined_y = 550, 80 | |
| combined_size = 180 | |
| cv2.putText(img, "COMBINED FIELD", (combined_x, combined_y - 5), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 200), 1) | |
| mag = np.abs(self.combined_field) | |
| phase = np.angle(self.combined_field) | |
| hsv = np.zeros((self.field_size, self.field_size, 3), dtype=np.uint8) | |
| hsv[:, :, 0] = ((phase + np.pi) / (2 * np.pi) * 180).astype(np.uint8) | |
| hsv[:, :, 1] = 200 | |
| hsv[:, :, 2] = (mag / (mag.max() + 1e-10) * 255).astype(np.uint8) | |
| combined_color = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) | |
| combined_resized = cv2.resize(combined_color, (combined_size, combined_size)) | |
| img[combined_y:combined_y + combined_size, combined_x:combined_x + combined_size] = combined_resized | |
| # Global metrics | |
| global_sym = self._compute_symmetry_score(self.combined_field) | |
| global_ani = self._compute_anisotropy(self.combined_field) | |
| cv2.putText(img, f"Global Sym: {global_sym:.3f}", (combined_x, combined_y + combined_size + 20), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 200), 1) | |
| cv2.putText(img, f"Global Ani: {global_ani:.3f}", (combined_x, combined_y + combined_size + 40), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 200), 1) | |
| # Criticality score | |
| criticality = global_sym * (1 - global_ani) | |
| cv2.putText(img, f"CRITICALITY: {criticality:.3f}", (combined_x, combined_y + combined_size + 65), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.5, (100, 255, 100) if criticality > 0.3 else (255, 100, 100), 1) | |
| # === INTER-COUPLING MATRIX === | |
| matrix_x, matrix_y = 780, 80 | |
| cell_size = 40 | |
| cv2.putText(img, "INTER-OPTICS κ_ij", (matrix_x, matrix_y - 5), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 150), 1) | |
| for i in range(self.N): | |
| for j in range(self.N): | |
| x = matrix_x + j * cell_size | |
| y = matrix_y + i * cell_size | |
| val = self.inter_kappa[i, j] | |
| intensity = int(val * 255) | |
| if i == j: | |
| color = (40, 40, 40) | |
| else: | |
| color = (intensity, intensity // 2, 50) | |
| cv2.rectangle(img, (x, y), (x + cell_size - 2, y + cell_size - 2), color, -1) | |
| cv2.putText(img, f"{val:.1f}", (x + 5, y + 25), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 255, 255), 1) | |
| # === COALITION MATRIX === | |
| coal_x, coal_y = 780, 280 | |
| cv2.putText(img, "COALITIONS", (coal_x, coal_y - 5), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 150), 1) | |
| for i in range(self.N): | |
| for j in range(self.N): | |
| x = coal_x + j * cell_size | |
| y = coal_y + i * cell_size | |
| val = self.coalition_matrix[i, j] | |
| intensity = int(val * 255) | |
| color = (50, intensity, intensity // 2) | |
| cv2.rectangle(img, (x, y), (x + cell_size - 2, y + cell_size - 2), color, -1) | |
| # === STATE DISPLAY === | |
| state_x, state_y = 20, 280 | |
| cv2.putText(img, "ATTRACTOR STATES", (state_x, state_y), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 150), 1) | |
| for i in range(self.N): | |
| y = state_y + 25 + i * 30 | |
| amp = np.abs(self.states[i]) | |
| phase = np.angle(self.states[i]) | |
| # Amplitude bar | |
| bar_width = int(min(amp * 50, 150)) | |
| cv2.rectangle(img, (state_x, y), (state_x + bar_width, y + 15), self.attractor_colors[i], -1) | |
| # Phase indicator | |
| phase_x = state_x + 160 + int(20 * np.cos(phase)) | |
| phase_y = y + 7 + int(7 * np.sin(phase)) | |
| cv2.circle(img, (phase_x, phase_y), 4, (255, 255, 255), -1) | |
| cv2.putText(img, f"{self.attractor_names[i]}: |{amp:.1f}| ∠{np.degrees(phase):.0f}°", | |
| (state_x + 200, y + 12), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.3, self.attractor_colors[i], 1) | |
| # === FIELD KAPPA DISPLAY === | |
| kappa_x, kappa_y = 20, 420 | |
| cv2.putText(img, "FIELD COUPLING κ_i (integration windows)", (kappa_x, kappa_y), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 150), 1) | |
| for i in range(self.N): | |
| y = kappa_y + 20 + i * 25 | |
| kappa = self.field_kappa[i] | |
| bar_width = int(kappa * 100) | |
| # Color based on regime | |
| if kappa < 0.3: | |
| color = (150, 150, 100) # Low - soup | |
| regime = "SOUP" | |
| elif kappa > 0.8: | |
| color = (100, 100, 200) # High - stripes | |
| regime = "STRIPES" | |
| else: | |
| color = (100, 200, 100) # Critical | |
| regime = "CRITICAL" | |
| cv2.rectangle(img, (kappa_x, y), (kappa_x + bar_width, y + 15), color, -1) | |
| cv2.putText(img, f"{self.attractor_names[i]}: κ={kappa:.2f} [{regime}]", | |
| (kappa_x + 120, y + 12), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.3, color, 1) | |
| # === THEORY BOX === | |
| theory_y = 560 | |
| cv2.putText(img, "OPTICS OF INFORMATION:", (20, theory_y), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.4, (150, 200, 150), 1) | |
| cv2.putText(img, "F_eff(t) = (S * K_kappa)(t) | kappa = integration window = spectral bandwidth", | |
| (20, theory_y + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (120, 150, 120), 1) | |
| cv2.putText(img, "Low kappa -> averaging -> soup | Critical kappa -> interference -> lattice | High kappa -> aliasing -> stripes", | |
| (20, theory_y + 40), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (120, 150, 120), 1) | |
| cv2.putText(img, "Multiple attractors can tune optics BETWEEN each other -> coalition formation -> attention", | |
| (20, theory_y + 60), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (120, 150, 120), 1) | |
| # === DOMINANT ATTRACTOR === | |
| dominant = np.argmax(self.symmetry_scores) | |
| cv2.putText(img, f"Dominant: {self.attractor_names[dominant]}", (750, theory_y + 20), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.5, self.attractor_colors[dominant], 1) | |
| # === PARAMETERS === | |
| cv2.putText(img, f"zoom={self.lattice_zoom:.1f} | freq={self.lattice_freq:.1f} | adapt={self.adaptation_rate:.3f}", | |
| (20, 680), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (100, 100, 100), 1) | |
| self._display = img | |
| def get_output(self, name): | |
| if name == 'display': | |
| return self._display | |
| elif name == 'swarm_field': | |
| return self.combined_field | |
| elif name == 'coupling_matrix': | |
| return self.inter_kappa.flatten() | |
| elif name == 'coalition_labels': | |
| # Simple coalition detection: threshold the matrix | |
| labels = np.zeros(self.N) | |
| for i in range(self.N): | |
| labels[i] = np.argmax(self.coalition_matrix[i, :]) | |
| return labels | |
| elif name == 'dominant_attractor': | |
| return float(np.argmax(self.symmetry_scores)) | |
| elif name == 'global_symmetry': | |
| return float(self._compute_symmetry_score(self.combined_field)) | |
| elif name == 'anisotropy': | |
| return float(self._compute_anisotropy(self.combined_field)) | |
| elif name == 'criticality_score': | |
| sym = self._compute_symmetry_score(self.combined_field) | |
| ani = self._compute_anisotropy(self.combined_field) | |
| return float(sym * (1 - ani)) | |
| return None | |
| def get_display_image(self): | |
| h, w = self._display.shape[:2] | |
| return QtGui.QImage(self._display.data, w, h, w * 3, | |
| QtGui.QImage.Format.Format_RGB888) | |
| def get_config_options(self): | |
| return [ | |
| ("Adaptation Rate", "adaptation_rate", self.adaptation_rate, None), | |
| ("Lattice Zoom", "lattice_zoom", self.lattice_zoom, None), | |
| ("Lattice Freq", "lattice_freq", self.lattice_freq, None), | |
| ] |