Spaces:
Running
Running
| """ | |
| 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) | |
| ] |