PerceptionLabPortable / app /nodes /adaptiveeigenfienldnode.py
Aluode's picture
Upload folder using huggingface_hub
3bb804c verified
"""
Adaptive Eigenfield Node
========================
"The field becomes the limiting factor."
This node synthesizes several key insights:
1. From selfconsistentresonantloopnode: harmonics naturally produce structure
(1→block, 2→complex, 6→star, higher→breakdown). The system should derive
harmonics from the signal, not hardcode them.
2. From best.py: stable patterns can be detected and tracked. When coherent
regions persist, they become "cells" - exactly like morphogenetic fields.
3. From the Raj paper: brain eigenmodes are conserved low-frequency patterns
that govern diffusion. Low eigenmodes = coarse structure, high = fine detail.
4. From the DNA/THz papers: resonant frequencies emerge from geometry and coupling.
The system has natural frequencies determined by its structure.
The node:
- Derives num_waves from spectral peaks in input (adaptive harmonics)
- Computes graph Laplacian eigenmodes for field topology
- Projects eigenmodes onto the field with amplitude/phase from signal
- Detects stable coherent regions (cells)
- Zoom selects which eigenmodes dominate (low zoom = slow modes, high = fast)
- Field limits harmony at high complexity (biological reality)
CREATED: December 2025
AUTHORS: Antti + Claude
"""
import numpy as np
import cv2
from collections import deque
from scipy.fft import fft2, ifft2, fftshift, fft, fftfreq
from scipy.ndimage import gaussian_filter, label, binary_erosion, binary_dilation
from scipy.signal import find_peaks
from scipy.sparse import diags
from scipy.sparse.linalg import eigsh
# --- HOST COMMUNICATION ---
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 StablePattern:
"""A detected stable structure - like a cell or coherent domain"""
def __init__(self, id, mask, position, volume, phase_coherence):
self.id = id
self.mask = mask.copy()
self.position = position # Center of mass
self.volume = volume
self.phase_coherence = phase_coherence
self.age = 0
self.color = np.random.rand(3) # For visualization
def update(self, new_mask=None, new_position=None, new_coherence=None):
if new_mask is not None:
self.mask = new_mask.copy()
if new_position is not None:
self.position = new_position
if new_coherence is not None:
self.phase_coherence = new_coherence
self.age += 1
class AdaptiveEigenfieldNode(BaseNode):
NODE_CATEGORY = "Consciousness"
NODE_TITLE = "Adaptive Eigenfield"
NODE_COLOR = QtGui.QColor(180, 100, 255) # Purple for eigenmodes
def __init__(self):
super().__init__()
self.node_title = "Adaptive Eigenfield (Signal-Derived Eigenmodes)"
self.inputs = {
'eeg_signal': 'signal', # Raw signal to buffer
'eeg_spectrum': 'spectrum', # Direct spectrum input (6-band)
'frequency_input': 'spectrum', # Alternative spectrum
'zoom': 'signal', # Eigenmode selection (0=slow only, 1=all)
'coupling': 'signal', # Field coupling strength
'damping': 'signal', # Energy dissipation
'tension': 'signal', # Wave propagation speed
'topology': 'signal', # 0=box, 1=torus
'reset': 'signal'
}
self.outputs = {
'display': 'image',
'field': 'complex_spectrum', # The main eigenfield
'eigenspectrum': 'spectrum', # Current eigenvalues
'num_modes': 'signal', # Number of active eigenmodes
'num_patterns': 'signal', # Detected stable patterns
'criticality': 'signal', # Edge of chaos metric
'total_energy': 'signal',
'pattern_field': 'image', # Visualization of stable patterns
}
# Field parameters
self.field_size = 128
self.dt = 0.1
self.damping = 0.001
self.tension = 5.0
self.coupling = 0.5
self.zoom = 0.5 # 0 = show only slowest modes, 1 = all modes
self.topology = 'box' # 'box' or 'torus'
# Signal processing
self.buffer_size = 512
self.sample_rate = 160.0
self.signal_buffer = deque(maxlen=self.buffer_size)
# Eigenmode system
self.max_modes = 32 # Maximum number of eigenmodes to compute
self._eigenvectors = None
self._eigenvalues = None
self._mode_amplitudes = np.zeros(self.max_modes)
self._mode_phases = np.zeros(self.max_modes)
self._num_active_modes = 6 # Derived from signal peaks
# Initialize eigenmodes
self._compute_laplacian_eigenmodes()
# Field state (like best.py)
self.field = np.zeros((self.field_size, self.field_size), dtype=np.complex128)
self.field_prev = np.zeros_like(self.field)
self._init_field()
# Stable pattern tracking
self.patterns = {}
self.next_pattern_id = 1
self.pattern_mask = np.zeros((self.field_size, self.field_size), dtype=bool)
self.last_detection_time = 0
# Metrics
self.total_energy = 0.0
self.energy_history = deque(maxlen=200)
self.criticality = 0.0
self.criticality_history = deque(maxlen=200)
# Display
self._display = np.zeros((600, 900, 3), dtype=np.uint8)
self.epoch = 0
def _compute_laplacian_eigenmodes(self):
"""Compute eigenmodes of the 2D grid Laplacian"""
n = self.field_size
n_sq = n * n
# Build sparse Laplacian matrix for 2D grid
# Each point connected to 4 neighbors (or wrapped for torus)
main_diag = np.ones(n_sq) * 4
off_diag = np.ones(n_sq - 1) * -1
# Handle row boundaries (no connection across rows)
for i in range(n - 1, n_sq - 1, n):
off_diag[i] = 0
row_diag = np.ones(n_sq - n) * -1
L = diags([main_diag, off_diag, off_diag, row_diag, row_diag],
[0, -1, 1, -n, n], format='csr')
# Compute smallest eigenvalues (slowest modes)
try:
eigenvalues, eigenvectors = eigsh(L.astype(np.float64),
k=min(self.max_modes, n_sq - 2),
which='SM')
self._eigenvalues = eigenvalues
self._eigenvectors = eigenvectors
except Exception as e:
print(f"Eigenmode computation failed: {e}")
# Fallback to simple sine modes
self._eigenvalues = np.arange(1, self.max_modes + 1).astype(float)
self._eigenvectors = np.zeros((n_sq, self.max_modes))
for m in range(self.max_modes):
# Simple standing wave approximation
kx = (m % 8) + 1
ky = (m // 8) + 1
x = np.arange(n)
X, Y = np.meshgrid(x, x)
mode = np.sin(np.pi * kx * X / n) * np.sin(np.pi * ky * Y / n)
self._eigenvectors[:, m] = mode.flatten()
def _init_field(self):
"""Initialize field with small random perturbation"""
n = self.field_size
c = n // 2
r = n // 6
X, Y = np.meshgrid(np.arange(n), np.arange(n))
# Gaussian seed + small noise
self.field = 0.5 * np.exp(-((X - c)**2 + (Y - c)**2) / (2 * r**2))
self.field = self.field.astype(np.complex128)
self.field += (np.random.randn(n, n) + 1j * np.random.randn(n, n)) * 0.05
self.field_prev = self.field.copy()
def _derive_modes_from_signal(self):
"""Derive number of active modes from spectral peaks in input"""
if len(self.signal_buffer) < self.buffer_size // 4:
return
try:
sig = np.array(list(self.signal_buffer))
sig = sig - np.mean(sig)
if np.std(sig) < 1e-10:
return
# FFT
spectrum = np.abs(fft(sig * np.hanning(len(sig))))
freqs = fftfreq(len(sig), 1.0 / self.sample_rate)
# Only positive frequencies
pos_mask = freqs > 0
spectrum_pos = spectrum[pos_mask]
if len(spectrum_pos) == 0:
return
# Find peaks
threshold = np.mean(spectrum_pos) * 1.5
peaks, properties = find_peaks(spectrum_pos, height=threshold, distance=5)
# Number of significant peaks determines mode count
num_peaks = len(peaks)
if num_peaks == 0:
self._num_active_modes = 2 # Minimum
elif num_peaks == 1:
self._num_active_modes = 4
elif num_peaks <= 3:
self._num_active_modes = 6
elif num_peaks <= 6:
self._num_active_modes = 12
else:
self._num_active_modes = min(num_peaks * 2, self.max_modes)
# Set mode amplitudes from peak heights
self._mode_amplitudes[:] = 0
if num_peaks > 0:
heights = properties['peak_heights']
max_height = np.max(heights) if len(heights) > 0 else 1.0
for i, (peak_idx, height) in enumerate(zip(peaks, heights)):
if i < self.max_modes:
self._mode_amplitudes[i] = height / max_height
# Phase from signal phase at that frequency
self._mode_phases[i] = np.angle(fft(sig)[pos_mask][peak_idx]) if peak_idx < len(spectrum_pos) else 0
except Exception as e:
pass # Keep previous mode count
def _project_eigenmodes_to_field(self):
"""Project active eigenmodes onto the 2D field with zoom-based selection"""
if self._eigenvectors is None:
return np.zeros((self.field_size, self.field_size), dtype=np.complex128)
n = self.field_size
result = np.zeros((n, n), dtype=np.complex128)
# Zoom determines which modes are active
# zoom=0: only mode 0 (slowest)
# zoom=1: all modes up to num_active
max_mode_idx = max(1, int(self.zoom * self._num_active_modes))
max_mode_idx = min(max_mode_idx, self._eigenvectors.shape[1])
for m in range(max_mode_idx):
if m >= len(self._mode_amplitudes):
break
amp = self._mode_amplitudes[m]
phase = self._mode_phases[m]
if amp < 1e-6:
amp = 0.1 # Default amplitude for unset modes
# Get eigenmode and reshape to 2D
if m < self._eigenvectors.shape[1]:
mode_1d = self._eigenvectors[:, m]
mode_2d = mode_1d.reshape(n, n)
# Add with amplitude and phase
result += amp * mode_2d * np.exp(1j * phase)
# Normalize
max_val = np.max(np.abs(result))
if max_val > 1e-10:
result /= max_val
return result
def _step_field_physics(self):
"""Evolve field with wave equation physics (inspired by best.py)"""
n = self.field_size
# Get current eigenmode projection
eigenmode_contribution = self._project_eigenmodes_to_field()
# Boundary mode based on topology
mode = 'wrap' if self.topology == 'torus' else 'reflect'
# Laplacian via convolution
kernel = np.array([[0, 1, 0],
[1, -4, 1],
[0, 1, 0]], dtype=np.float64)
# Apply to real and imaginary parts
lap_real = cv2.filter2D(np.real(self.field).astype(np.float64), -1, kernel,
borderType=cv2.BORDER_WRAP if mode == 'wrap' else cv2.BORDER_REFLECT)
lap_imag = cv2.filter2D(np.imag(self.field).astype(np.float64), -1, kernel,
borderType=cv2.BORDER_WRAP if mode == 'wrap' else cv2.BORDER_REFLECT)
lap = lap_real + 1j * lap_imag
# Wave equation with damping
# φ_new = 2φ - φ_old + dt²(c²∇²φ - V'(φ)) - damping*(φ - φ_old)
# Non-linear potential (encourages phase coherence)
mag = np.abs(self.field)
V_prime = -self.field + 0.2 * self.field * mag**2
# Wave speed modulated by tension
c2 = self.tension / (1.0 + mag**2 + 1e-6)
acc = c2 * lap - V_prime
# Velocity
vel = self.field - self.field_prev
# Update
field_new = (self.field + (1 - self.damping * self.dt) * vel +
self.dt**2 * acc)
# Couple in eigenmode structure
field_new = (1 - self.coupling * 0.01) * field_new + self.coupling * 0.01 * eigenmode_contribution
# Store history
self.field_prev = self.field.copy()
self.field = field_new
# Normalize to prevent blowup
max_mag = np.max(np.abs(self.field))
if max_mag > 5.0:
self.field /= (max_mag / 5.0)
def _detect_stable_patterns(self):
"""Detect coherent regions in the field (cells)"""
# Only run periodically
import time
current_time = time.time()
if current_time - self.last_detection_time < 0.3:
return
self.last_detection_time = current_time
n = self.field_size
# Coherence = local phase consistency
phase = np.angle(self.field)
# Compute local phase variance (low = coherent)
phase_blurred = gaussian_filter(phase, sigma=3)
phase_diff = np.abs(phase - phase_blurred)
coherence = 1.0 - np.clip(phase_diff / np.pi, 0, 1)
# Also consider magnitude
mag = np.abs(self.field)
mag_norm = mag / (np.max(mag) + 1e-10)
# Pattern mask: high coherence AND significant magnitude
pattern_criterion = coherence * mag_norm
binary_mask = pattern_criterion > 0.5
# Clean up
binary_mask = binary_erosion(binary_mask, iterations=1)
binary_mask = binary_dilation(binary_mask, iterations=2)
# Label connected components
labeled, num_features = label(binary_mask)
# Track patterns
active_ids = set()
min_volume = 20
for i in range(1, num_features + 1):
component_mask = (labeled == i)
volume = np.sum(component_mask)
if volume < min_volume:
continue
# Get centroid
coords = np.where(component_mask)
position = (np.mean(coords[0]), np.mean(coords[1]))
# Mean phase coherence in this region
region_coherence = np.mean(coherence[component_mask])
# Try to match with existing pattern
matched = False
closest_id = None
min_dist = float('inf')
for pid, pattern in self.patterns.items():
dist = np.sqrt((pattern.position[0] - position[0])**2 +
(pattern.position[1] - position[1])**2)
if dist < min_dist:
min_dist = dist
closest_id = pid
if closest_id is not None and min_dist < 15:
self.patterns[closest_id].update(component_mask, position, region_coherence)
active_ids.add(closest_id)
matched = True
if not matched:
new_id = self.next_pattern_id
self.next_pattern_id += 1
self.patterns[new_id] = StablePattern(new_id, component_mask, position,
volume, region_coherence)
active_ids.add(new_id)
# Age out patterns not detected
to_remove = []
for pid in self.patterns:
if pid not in active_ids:
self.patterns[pid].age -= 2
if self.patterns[pid].age <= 0:
to_remove.append(pid)
for pid in to_remove:
del self.patterns[pid]
# Update global mask
self.pattern_mask = np.zeros((n, n), dtype=bool)
for pattern in self.patterns.values():
self.pattern_mask |= pattern.mask
def _compute_metrics(self):
"""Compute energy and criticality metrics"""
# Energy
mag = np.abs(self.field)
grad_x = np.gradient(np.real(self.field), axis=0)
grad_y = np.gradient(np.real(self.field), axis=1)
kinetic = 0.5 * np.sum(np.abs(self.field - self.field_prev)**2)
potential = 0.5 * np.sum(grad_x**2 + grad_y**2)
self.total_energy = kinetic + potential
self.energy_history.append(self.total_energy)
# Criticality: variance of energy history (high variance = critical)
if len(self.energy_history) > 10:
energy_arr = np.array(list(self.energy_history))
mean_e = np.mean(energy_arr)
if mean_e > 1e-10:
self.criticality = np.std(energy_arr) / mean_e
else:
self.criticality = 0.0
self.criticality = np.clip(self.criticality, 0, 1)
self.criticality_history.append(self.criticality)
def step(self):
self.epoch += 1
# Get inputs
reset = self.get_blended_input('reset', 'sum')
if reset is not None and reset > 0.5:
self._init_field()
self.patterns = {}
self.next_pattern_id = 1
self.energy_history.clear()
self.criticality_history.clear()
return
# Update parameters from inputs
zoom_in = self.get_blended_input('zoom', 'sum')
if zoom_in is not None:
self.zoom = np.clip(float(zoom_in), 0, 1)
coupling_in = self.get_blended_input('coupling', 'sum')
if coupling_in is not None:
self.coupling = np.clip(float(coupling_in), 0, 1)
damping_in = self.get_blended_input('damping', 'sum')
if damping_in is not None:
self.damping = np.clip(float(damping_in), 0, 0.1)
tension_in = self.get_blended_input('tension', 'sum')
if tension_in is not None:
self.tension = np.clip(float(tension_in), 0.1, 20)
topology_in = self.get_blended_input('topology', 'sum')
if topology_in is not None:
self.topology = 'torus' if float(topology_in) > 0.5 else 'box'
# Buffer signal
sig_in = self.get_blended_input('eeg_signal', 'sum')
if sig_in is not None:
if isinstance(sig_in, np.ndarray):
for s in sig_in.flatten()[:10]:
self.signal_buffer.append(float(s))
else:
self.signal_buffer.append(float(sig_in))
# Process spectrum input
spectrum_in = self.get_blended_input('eeg_spectrum', 'sum')
if spectrum_in is None:
spectrum_in = self.get_blended_input('frequency_input', 'sum')
if spectrum_in is not None and isinstance(spectrum_in, np.ndarray):
# Use spectrum peaks to set mode amplitudes directly
spec = np.abs(spectrum_in)
if len(spec) > 0:
max_spec = np.max(spec)
if max_spec > 1e-10:
spec = spec / max_spec
for i, val in enumerate(spec[:self.max_modes]):
self._mode_amplitudes[i] = val
self._num_active_modes = max(2, min(len(spec), self.max_modes))
else:
# Derive modes from buffered signal
self._derive_modes_from_signal()
# Physics step
self._step_field_physics()
# Pattern detection
self._detect_stable_patterns()
# Metrics
self._compute_metrics()
# Update display
self._update_display()
def _update_display(self):
"""Generate visualization"""
img = np.zeros((600, 900, 3), dtype=np.uint8)
# Main field visualization (left side)
field_size_display = 256
# Field magnitude with phase as hue
mag = np.abs(self.field)
phase = np.angle(self.field)
mag_norm = mag / (np.max(mag) + 1e-10)
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_norm * 255).astype(np.uint8)
field_color = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
field_resized = cv2.resize(field_color, (field_size_display, field_size_display))
img[20:20 + field_size_display, 20:20 + field_size_display] = field_resized
cv2.putText(img, "EIGENFIELD (Phase=Hue, Mag=Bright)", (20, 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 200), 1)
# Pattern overlay
if np.any(self.pattern_mask):
pattern_overlay = np.zeros((self.field_size, self.field_size, 3), dtype=np.uint8)
for pattern in self.patterns.values():
c = (pattern.color * 255).astype(np.uint8)
pattern_overlay[pattern.mask] = c
pattern_resized = cv2.resize(pattern_overlay, (field_size_display, field_size_display))
# Blend
alpha = 0.3
img[20:20 + field_size_display, 20:20 + field_size_display] = \
cv2.addWeighted(field_resized, 1 - alpha, pattern_resized, alpha, 0)
# Eigenspectrum visualization (right top)
spec_x, spec_y = 300, 30
spec_w, spec_h = 250, 100
cv2.putText(img, f"ACTIVE EIGENMODES (n={self._num_active_modes})", (spec_x, spec_y - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 150), 1)
if self._eigenvalues is not None:
num_show = min(self._num_active_modes, len(self._eigenvalues))
max_amp = max(np.max(self._mode_amplitudes[:num_show]), 1e-10)
bar_width = max(3, spec_w // num_show - 2)
for i in range(num_show):
x = spec_x + i * (bar_width + 2)
amp = self._mode_amplitudes[i] / max_amp if i < len(self._mode_amplitudes) else 0
height = int(amp * spec_h)
# Color by eigenvalue (slow=red, fast=blue)
hue = int(120 * (i / max(num_show - 1, 1))) # Green to cyan
color = cv2.cvtColor(np.array([[[hue, 200, 200]]], dtype=np.uint8),
cv2.COLOR_HSV2BGR)[0, 0]
cv2.rectangle(img, (x, spec_y + spec_h - height),
(x + bar_width, spec_y + spec_h),
tuple(int(c) for c in color), -1)
# Zoom indicator
zoom_y = spec_y + spec_h + 30
cv2.putText(img, f"ZOOM: {self.zoom:.2f}", (spec_x, zoom_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 200), 1)
cv2.rectangle(img, (spec_x + 80, zoom_y - 10),
(spec_x + 80 + int(self.zoom * 100), zoom_y),
(100, 200, 255), -1)
cv2.putText(img, "low=coarse structure | high=fine detail", (spec_x, zoom_y + 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.3, (150, 150, 150), 1)
# Metrics panel
metrics_x, metrics_y = 300, 200
cv2.putText(img, "METRICS", (metrics_x, metrics_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 150), 1)
cv2.putText(img, f"Energy: {self.total_energy:.2f}", (metrics_x, metrics_y + 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1)
cv2.putText(img, f"Criticality: {self.criticality:.3f}", (metrics_x, metrics_y + 50),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 200, 100), 1)
bar_w = int(self.criticality * 150)
cv2.rectangle(img, (metrics_x, metrics_y + 55),
(metrics_x + bar_w, metrics_y + 65),
(100, 200, 255), -1)
cv2.putText(img, f"Stable Patterns: {len(self.patterns)}", (metrics_x, metrics_y + 90),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (100, 255, 100), 1)
cv2.putText(img, f"Topology: {self.topology.upper()}", (metrics_x, metrics_y + 115),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 200), 1)
# Pattern list
pattern_x, pattern_y = 300, 340
cv2.putText(img, "DETECTED CELLS (age/coherence):", (pattern_x, pattern_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 150), 1)
for i, (pid, pattern) in enumerate(list(self.patterns.items())[:8]):
c = (pattern.color * 255).astype(np.uint8)
y = pattern_y + 20 + i * 20
cv2.rectangle(img, (pattern_x, y - 10), (pattern_x + 15, y + 5),
tuple(int(x) for x in c), -1)
cv2.putText(img, f"#{pid}: age={pattern.age}, coh={pattern.phase_coherence:.2f}",
(pattern_x + 20, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (200, 200, 200), 1)
# Criticality history
hist_x, hist_y = 20, 320
hist_w, hist_h = 250, 80
cv2.putText(img, "CRITICALITY HISTORY", (hist_x, hist_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 150), 1)
if len(self.criticality_history) > 2:
hist = np.array(list(self.criticality_history))
hist = hist / (np.max(hist) + 1e-10)
for i in range(1, len(hist)):
x1 = hist_x + int((i - 1) / len(hist) * hist_w)
x2 = hist_x + int(i / len(hist) * hist_w)
y1 = hist_y + 10 + hist_h - int(hist[i - 1] * hist_h)
y2 = hist_y + 10 + hist_h - int(hist[i] * hist_h)
cv2.line(img, (x1, y1), (x2, y2), (100, 200, 255), 1)
# Theory notes
theory_y = 450
cv2.putText(img, "ADAPTIVE EIGENFIELD HYPOTHESIS:", (20, theory_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (150, 200, 150), 1)
cv2.putText(img, "Eigenmodes derived from input signal spectrum, not hardcoded.",
(20, theory_y + 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (120, 150, 120), 1)
cv2.putText(img, f"Simple signal -> few modes -> blocks. Complex -> many -> stars -> breakdown.",
(20, theory_y + 40),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (120, 150, 120), 1)
cv2.putText(img, "ZOOM selects eigenmode range: 0=slow(coarse), 1=fast(fine)",
(20, theory_y + 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (120, 150, 120), 1)
cv2.putText(img, "Stable regions = 'cells' with coherent phase. Field limits harmony at complexity.",
(20, theory_y + 80),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (120, 150, 120), 1)
# Parameters
cv2.putText(img, f"epoch={self.epoch} | coupling={self.coupling:.2f} | damping={self.damping:.4f} | tension={self.tension:.1f}",
(20, 580), 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 == 'field':
return self.field
elif name == 'eigenspectrum':
if self._eigenvalues is not None:
return self._eigenvalues[:self._num_active_modes]
return np.zeros(6)
elif name == 'num_modes':
return float(self._num_active_modes)
elif name == 'num_patterns':
return float(len(self.patterns))
elif name == 'criticality':
return float(self.criticality)
elif name == 'total_energy':
return float(self.total_energy)
elif name == 'pattern_field':
# Return visualization of just the patterns
img = np.zeros((self.field_size, self.field_size), dtype=np.uint8)
for pattern in self.patterns.values():
brightness = int(pattern.age * 10)
img[pattern.mask] = min(255, brightness)
return img
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 [
("Zoom (Eigenmode Selection)", "zoom", self.zoom, None),
("Coupling", "coupling", self.coupling, None),
("Damping", "damping", self.damping, None),
("Tension", "tension", self.tension, None),
("Topology", "topology", self.topology, [("Box", "box"), ("Torus", "torus")]),
]