""" 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), ]