PerceptionLabPortable / app /nodes /PCAautoexplorernode.py
Aluode's picture
Upload folder using huggingface_hub
3bb804c verified
"""
Auto-Explorer Node - Automatically animates through PC space
Creates smooth explorations of the learned manifold
"""
import numpy as np
from PyQt6 import QtGui
import cv2
import __main__
BaseNode = __main__.BaseNode
QtGui = __main__.QtGui
class AutoExplorerNode(BaseNode):
"""
Automatically explores PCA latent space with smooth animations.
Multiple modes: sequential, random walk, circular, spiral
"""
NODE_CATEGORY = "AI / Physics"
NODE_COLOR = QtGui.QColor(100, 220, 180)
def __init__(self, mode='sequential'):
super().__init__()
self.node_title = "Auto-Explorer"
self.inputs = {
'latent_in': 'spectrum',
'speed': 'signal',
'amplitude': 'signal',
'chaos': 'signal' # Randomness amount
}
self.outputs = {
'latent_out': 'spectrum',
'current_pc': 'signal',
'phase': 'signal' # 0-1 oscillation
}
self.mode = mode # 'sequential', 'random_walk', 'circular', 'spiral'
# State
self.base_latent = None
self.current_latent = None
self.phase = 0.0
self.current_pc = 0
self.random_state = np.random.randn(8) # For random walk
def step(self):
latent_in = self.get_blended_input('latent_in', 'first')
speed = self.get_blended_input('speed', 'sum') or 0.05
amplitude = self.get_blended_input('amplitude', 'sum') or 2.0
chaos = self.get_blended_input('chaos', 'sum') or 0.0
if latent_in is not None:
if self.base_latent is None:
self.base_latent = latent_in.copy()
self.current_latent = self.base_latent.copy()
# Advance phase
self.phase += speed
if self.mode == 'sequential':
self._sequential_mode(amplitude)
elif self.mode == 'random_walk':
self._random_walk_mode(amplitude, chaos)
elif self.mode == 'circular':
self._circular_mode(amplitude)
elif self.mode == 'spiral':
self._spiral_mode(amplitude)
def _sequential_mode(self, amplitude):
"""Oscillate through PCs one at a time"""
latent_dim = len(self.base_latent)
# Current PC index (cycles through all)
self.current_pc = int(self.phase / (2*np.pi)) % latent_dim
# Oscillate that PC
modulation = np.sin(self.phase) * amplitude
self.current_latent[self.current_pc] += modulation
def _random_walk_mode(self, amplitude, chaos):
"""Brownian motion in latent space"""
latent_dim = len(self.base_latent)
# Update random state
self.random_state += np.random.randn(latent_dim) * chaos * 0.1
# Apply damping
self.random_state *= 0.98
# Add to latent
for i in range(min(latent_dim, len(self.random_state))):
self.current_latent[i] += self.random_state[i] * amplitude
def _circular_mode(self, amplitude):
"""Rotate in PC0-PC1 plane"""
if len(self.base_latent) >= 2:
self.current_latent[0] += np.cos(self.phase) * amplitude
self.current_latent[1] += np.sin(self.phase) * amplitude
self.current_pc = 0 # Indicate using PC0-PC1
def _spiral_mode(self, amplitude):
"""Spiral outward in PC0-PC1 plane while oscillating PC2"""
if len(self.base_latent) >= 3:
# Expanding spiral
radius = (self.phase / (2*np.pi)) % 5.0 # Expand over 5 cycles
self.current_latent[0] += np.cos(self.phase) * radius * amplitude * 0.3
self.current_latent[1] += np.sin(self.phase) * radius * amplitude * 0.3
self.current_latent[2] += np.sin(self.phase * 2) * amplitude * 0.5
self.current_pc = 2 # Indicate complex motion
def get_output(self, port_name):
if port_name == 'latent_out':
return self.current_latent
elif port_name == 'current_pc':
return float(self.current_pc)
elif port_name == 'phase':
return (self.phase % (2*np.pi)) / (2*np.pi) # Normalized 0-1
return None
def get_display_image(self):
"""Show current exploration trajectory"""
img = np.zeros((256, 256, 3), dtype=np.uint8)
if self.current_latent is None:
cv2.putText(img, "Waiting for input...", (10, 128),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 1)
return QtGui.QImage(img.data, 256, 256, 256*3, QtGui.QImage.Format.Format_RGB888)
# Draw mode and state
mode_text = f"Mode: {self.mode}"
pc_text = f"PC: {self.current_pc}"
phase_text = f"Phase: {self.phase:.2f}"
cv2.putText(img, mode_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 1)
cv2.putText(img, pc_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,255), 1)
cv2.putText(img, phase_text, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200,200,200), 1)
# Visualize current latent code as bars
latent_dim = len(self.current_latent)
bar_width = max(1, 256 // latent_dim)
delta = self.current_latent - self.base_latent
delta_max = np.abs(delta).max()
if delta_max > 1e-6:
delta_norm = delta / delta_max
else:
delta_norm = delta
for i, val in enumerate(delta_norm):
x = i * bar_width
h = int(abs(val) * 80)
y_base = 200
if val >= 0:
color = (0, 255, 0)
y_start = y_base - h
y_end = y_base
else:
color = (0, 0, 255)
y_start = y_base
y_end = y_base + h
# Highlight current PC
if i == self.current_pc:
color = (255, 255, 0)
cv2.rectangle(img, (x, y_start), (x+bar_width-1, y_end), color, -1)
# Draw baseline
cv2.line(img, (0, 200), (256, 200), (100, 100, 100), 1)
return QtGui.QImage(img.data, 256, 256, 256*3, QtGui.QImage.Format.Format_RGB888)
def get_config_options(self):
return [
("Mode", "mode", self.mode, None)
]