File size: 6,786 Bytes
3bb804c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""

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