""" Eigen Crystal Viewer Node - V6 (High-Res Smoothing) =================================================== Displays eigenmode crystals from complex spectrum input. V6 FIX: - Changed Upscaling from "Nearest Neighbor" (Blocky) to "Bicubic" (Smooth). The Seed layer is only 32x32 pixels. Bicubic interpolation smooths the jagged edges into organic gradients, making it look "High Res". """ import numpy as np import cv2 from scipy.fft import fft2, ifft2, fftshift, ifftshift from scipy.ndimage import gaussian_filter import __main__ try: BaseNode = __main__.BaseNode QtGui = __main__.QtGui except AttributeError: class BaseNode: def get_blended_input(self, name, mode): return None import PyQt6.QtGui as QtGui class EigenCrystalViewerNode(BaseNode): """ Eigen Crystal Viewer - Shows eigenmode crystals from complex spectra. """ NODE_CATEGORY = "Intelligence" NODE_TITLE = "Eigen Crystal Viewer" NODE_COLOR = QtGui.QColor(200, 100, 255) # Crystal purple def __init__(self): super().__init__() self.inputs = { 'complex_spectrum_in': 'complex_spectrum', 'image_in': 'image', 'settle_steps': 'signal', 'diffusion': 'signal', 'phase_rate': 'signal', } self.outputs = { 'eigen_image': 'image', # The Spectral Crystal 'structure_image': 'image', # The Spatial Rings 'spectrum_out': 'spectrum', 'coherence': 'signal', } # Layer sizes self.layer_sizes = [32, 64, 128] self.n_layers = 3 # Physics parameters self.settle_steps = 20 self.diffusion = 0.5 self.phase_rate = 0.05 self.tension_rate = 0.1 self.threshold = 0.6 # Configuration self.output_layer_idx = 0 # 0=Seed(Small), 1=Growth(Med), 2=Field(Large) # Initialize layers self.layers = [] for size in self.layer_sizes: layer = { 'size': size, 'center': size // 2, 'structure': self._init_structure(size), 'tension': np.zeros((size, size), dtype=np.float32), 'r_grid': self._make_r_grid(size) } self.layers.append(layer) # Output storage self.eigenmodes = [np.zeros((s, s), dtype=np.float32) for s in self.layer_sizes] self.structures = [np.zeros((s, s), dtype=np.float32) for s in self.layer_sizes] self.current_coherence = 0.0 self.output_spectrum = np.zeros(64, dtype=np.float32) def _init_structure(self, size): structure = np.ones((size, size), dtype=np.complex128) structure += (np.random.randn(size, size) + 1j * np.random.randn(size, size)) * 0.1 return structure def _make_r_grid(self, size): center = size // 2 y, x = np.ogrid[:size, :size] return np.sqrt((x - center)**2 + (y - center)**2) def compute_eigenmode(self, layer): return np.abs(fftshift(fft2(layer['structure']))) def compute_coherence(self, layer): phase = np.angle(layer['structure']) return float(np.abs(np.mean(np.exp(1j * phase)))) def spectrum_to_chord(self, spectrum, n_harmonics=5): if spectrum is None or len(spectrum) == 0: return np.ones(n_harmonics) * 0.5 if spectrum.ndim > 1: spectrum = np.mean(np.abs(spectrum), axis=0) spectrum = np.abs(spectrum) if len(spectrum) >= n_harmonics: band_size = len(spectrum) // n_harmonics chord = np.array([ np.mean(spectrum[i*band_size:(i+1)*band_size]) for i in range(n_harmonics) ], dtype=np.float32) else: chord = np.interp( np.linspace(0, len(spectrum)-1, n_harmonics), np.arange(len(spectrum)), spectrum ).astype(np.float32) if chord.max() > 1e-9: chord = chord / chord.max() return chord def project_chord_to_rings(self, layer, chord): size = layer['size'] center = layer['center'] r_grid = layer['r_grid'] ring_width = center / len(chord) pattern = np.zeros((size, size), dtype=np.float32) for i, intensity in enumerate(chord): inner = i * ring_width outer = (i + 1) * ring_width mask = (r_grid >= inner) & (r_grid < outer) pattern[mask] = intensity return pattern def settle_layer(self, layer, chord): size = layer['size'] layer['structure'] = self._init_structure(size) layer['tension'][:] = 0 for step in range(self.settle_steps): input_2d = self.project_chord_to_rings(layer, chord) if input_2d.max() > 1e-9: input_2d = input_2d / input_2d.max() eigen = self.compute_eigenmode(layer) eigen_norm = eigen / (eigen.max() + 1e-9) resistance = input_2d * (1.0 - eigen_norm) layer['tension'] += resistance * self.tension_rate critical = layer['tension'] > self.threshold n_critical = np.sum(critical) if n_critical > 0: layer['structure'][critical] *= -1 layer['tension'][critical] = 0 layer['structure'] = ( gaussian_filter(np.real(layer['structure']), self.diffusion) + 1j * gaussian_filter(np.imag(layer['structure']), self.diffusion) ) layer['structure'] *= np.exp(1j * self.phase_rate) mag = np.abs(layer['structure']) layer['structure'][mag > 1.0] /= mag[mag > 1.0] return self.compute_coherence(layer), self.compute_eigenmode(layer) def eigenmode_to_spectrum(self, eigenmode): size = eigenmode.shape[0] center = size // 2 y, x = np.ogrid[:size, :size] r = np.sqrt((x - center)**2 + (y - center)**2).astype(int) r_max = min(center, 64) spectrum = np.zeros(r_max, dtype=np.float32) for i in range(r_max): mask = (r == i) if np.any(mask): spectrum[i] = np.mean(eigenmode[mask]) return spectrum def step(self): complex_in = self.get_blended_input('complex_spectrum_in', 'first') image_in = self.get_blended_input('image_in', 'first') settle = self.get_blended_input('settle_steps', 'sum') diff = self.get_blended_input('diffusion', 'sum') phase = self.get_blended_input('phase_rate', 'sum') if settle is not None: self.settle_steps = int(np.clip(settle, 5, 100)) if diff is not None: self.diffusion = float(np.clip(diff, 0.1, 2.0)) if phase is not None: self.phase_rate = float(np.clip(phase, 0.01, 0.2)) if complex_in is not None: chord = self.spectrum_to_chord(complex_in) elif image_in is not None: if image_in.ndim == 3: gray = np.mean(image_in, axis=2) else: gray = image_in.copy() if gray.max() > 1.0: gray = gray / 255.0 gray = cv2.resize(gray.astype(np.float32), (64, 64)) spectrum_2d = np.abs(fftshift(fft2(gray))) chord = self.spectrum_to_chord(spectrum_2d.flatten()) else: chord = np.ones(5, dtype=np.float32) * 0.5 total_coherence = 0.0 current_chord = chord.copy() for i, layer in enumerate(self.layers): coherence, eigenmode = self.settle_layer(layer, current_chord) total_coherence += coherence self.eigenmodes[i] = eigenmode self.structures[i] = np.abs(layer['structure']) spectrum = self.eigenmode_to_spectrum(eigenmode) current_chord = self.spectrum_to_chord(spectrum) self.current_coherence = total_coherence / self.n_layers self.output_spectrum = self.eigenmode_to_spectrum(self.eigenmodes[-1]) def get_output(self, port_name): idx = self.output_layer_idx if idx >= len(self.layer_sizes): idx = 0 if port_name == 'eigen_image': eigen = self.eigenmodes[idx] if eigen.max() > 0: eigen_log = np.log(1 + eigen) eigen_norm = eigen_log / (eigen_log.max() + 1e-9) # --- V6 FIX: High-Quality Bicubic Upscaling --- target_size = 256 eigen_upscaled = cv2.resize( eigen_norm.astype(np.float32), (target_size, target_size), interpolation=cv2.INTER_CUBIC ) eigen_uint8 = (eigen_upscaled * 255).astype(np.uint8) return cv2.applyColorMap(eigen_uint8, cv2.COLORMAP_JET) return np.zeros((256, 256, 3), dtype=np.uint8) elif port_name == 'structure_image': struct = self.structures[idx] if struct.max() > 0: struct_norm = struct / (struct.max() + 1e-9) # --- V6 FIX: High-Quality Bicubic Upscaling --- target_size = 256 struct_upscaled = cv2.resize( struct_norm.astype(np.float32), (target_size, target_size), interpolation=cv2.INTER_CUBIC ) struct_uint8 = (struct_upscaled * 255).astype(np.uint8) return cv2.applyColorMap(struct_uint8, cv2.COLORMAP_TWILIGHT) return np.zeros((256, 256, 3), dtype=np.uint8) elif port_name == 'spectrum_out': return self.output_spectrum elif port_name == 'coherence': return self.current_coherence return None def get_display_image(self): panel_size = 100 margin = 2 col_width = panel_size + margin width = col_width * 3 height = col_width * 3 + 30 display = np.zeros((height, width, 3), dtype=np.uint8) for row, layer in enumerate(self.layers): y_start = row * col_width # Panel 1: Structure (Twilight) struct_mag = np.abs(layer['structure']) struct_mag = struct_mag / (struct_mag.max() + 1e-9) struct_img = cv2.resize(struct_mag.astype(np.float32), (panel_size, panel_size)) struct_color = cv2.applyColorMap((struct_img * 255).astype(np.uint8), cv2.COLORMAP_TWILIGHT) display[y_start:y_start+panel_size, 0:panel_size] = struct_color # Panel 2: Tension (Bone) tension_img = cv2.resize(layer['tension'].astype(np.float32), (panel_size, panel_size)) if tension_img.max() > 0: tension_img = tension_img / tension_img.max() tension_color = cv2.applyColorMap((tension_img * 255).astype(np.uint8), cv2.COLORMAP_BONE) display[y_start:y_start+panel_size, col_width:col_width+panel_size] = tension_color # Panel 3: Eigenmode (Jet) eigen = self.compute_eigenmode(layer) eigen_log = np.log(1 + eigen) eigen_norm = eigen_log / (eigen_log.max() + 1e-9) eigen_img = cv2.resize(eigen_norm.astype(np.float32), (panel_size, panel_size)) eigen_color = cv2.applyColorMap((eigen_img * 255).astype(np.uint8), cv2.COLORMAP_JET) display[y_start:y_start+panel_size, col_width*2:col_width*2+panel_size] = eigen_color if row == self.output_layer_idx: cv2.rectangle(display, (0, y_start), (width, y_start+panel_size), (255, 255, 255), 1) font = cv2.FONT_HERSHEY_SIMPLEX cv2.putText(display, "Struct", (10, 15), font, 0.4, (150, 150, 150), 1) cv2.putText(display, "Tension", (col_width + 10, 15), font, 0.4, (150, 150, 150), 1) cv2.putText(display, "Eigen", (col_width*2 + 10, 15), font, 0.4, (150, 150, 150), 1) status_y = height - 15 cv2.putText(display, f"Coh: {self.current_coherence:.2f}", (10, status_y), font, 0.4, (200, 200, 200), 1) layers = ["Seed", "Grow", "Field"] selected = layers[min(self.output_layer_idx, 2)] cv2.putText(display, f"Out: {selected}", (100, status_y), font, 0.4, (100, 255, 100), 1) return display def get_config_options(self): layer_opts = [ ('Seed (Small - 32px)', 0), ('Growth (Med - 64px)', 1), ('Field (Large - 128px)', 2) ] return [ ("Output Layer", "output_layer_idx", self.output_layer_idx, layer_opts), ("Settle Steps", "settle_steps", self.settle_steps, None), ("Diffusion", "diffusion", self.diffusion, None), ("Phase Rate", "phase_rate", self.phase_rate, None), ("Tension Rate", "tension_rate", self.tension_rate, None), ("Threshold", "threshold", self.threshold, None), ] def set_config_options(self, options): for key, value in options.items(): if hasattr(self, key): setattr(self, key, type(getattr(self, key))(value))