Spaces:
Running
Running
| """ | |
| Antti's Superfluid Node - Simulates a 1D complex field with knots | |
| Physics based on the 1D NLSE from knotiverse_interactive_viewer.py | |
| Requires: pip install scipy | |
| Place this file in the 'nodes' folder | |
| """ | |
| import numpy as np | |
| from PyQt6 import QtGui | |
| import cv2 | |
| import sys | |
| import os | |
| # --- This is the new, correct block --- | |
| import __main__ | |
| BaseNode = __main__.BaseNode | |
| PA_INSTANCE = getattr(__main__, "PA_INSTANCE", None) | |
| # ------------------------------------ | |
| try: | |
| from scipy.signal import hilbert | |
| SCIPY_AVAILABLE = True | |
| except ImportError: | |
| SCIPY_AVAILABLE = False | |
| print("Warning: AnttiSuperfluidNode requires 'scipy'.") | |
| print("Please run: pip install scipy") | |
| class AnttiSuperfluidNode(BaseNode): | |
| NODE_CATEGORY = "Transform" | |
| NODE_COLOR = QtGui.QColor(180, 80, 180) # Superfluid purple | |
| def __init__(self, grid_size=512, coupling=0.5, nonlinear=0.8, damping=0.005): | |
| super().__init__() | |
| self.node_title = "Antti's Superfluid" | |
| self.inputs = { | |
| 'signal_in': 'signal', | |
| 'coupling': 'signal', | |
| 'nonlinearity': 'signal', | |
| 'damping': 'signal' | |
| } | |
| self.outputs = { | |
| 'field_image': 'image', | |
| 'angular_momentum': 'signal', | |
| 'knot_count': 'signal' | |
| } | |
| # --- Parameters from knotiverse_interactive_viewer.py --- | |
| self.L = int(grid_size) | |
| self.dt = 0.05 | |
| self.detect_threshold = 0.5 | |
| self.saturation_threshold = 2.0 | |
| self.max_amplitude_clip = 1e3 | |
| # Default physics values (will be overridden by signals) | |
| self.coupling = coupling | |
| self.nonlinear = nonlinear | |
| self.damping = damping | |
| # --- Internal State --- | |
| rng = np.random.default_rng() | |
| self.psi = (rng.standard_normal(self.L) + 1j * rng.standard_normal(self.L)) * 0.01 | |
| # Seed with a pulse | |
| x = np.arange(self.L) | |
| p = self.L // 2 | |
| gauss = 1.0 * np.exp(-((x - p)**2) / (2 * 4**2)) | |
| self.psi += gauss * np.exp(1j * 2.0 * np.pi * rng.random()) | |
| self.knots = np.array([], dtype=int) | |
| self.angular_momentum_out = 0.0 | |
| if not SCIPY_AVAILABLE: | |
| self.node_title = "Superfluid (No SciPy!)" | |
| def laplacian_1d(self, arr): | |
| """Discrete laplacian with periodic boundary.""" | |
| return np.roll(arr, -1) - 2*arr + np.roll(arr, 1) | |
| def step(self): | |
| if not SCIPY_AVAILABLE: | |
| return | |
| # --- Get inputs --- | |
| signal_in = self.get_blended_input('signal_in', 'sum') or 0.0 | |
| coupling = self.get_blended_input('coupling', 'sum') | |
| nonlinear = self.get_blended_input('nonlinearity', 'sum') | |
| damping = self.get_blended_input('damping', 'sum') | |
| # Use signal if connected, else use internal value | |
| c = coupling if coupling is not None else self.coupling | |
| n = nonlinear if nonlinear is not None else self.nonlinear | |
| d = damping if damping is not None else self.damping | |
| # --- Physics Step (from knotiverse_interactive_viewer.py) --- | |
| lap = self.laplacian_1d(self.psi) | |
| coupling_term = 1j * c * lap | |
| amp = np.abs(self.psi) | |
| sat = np.tanh(amp / self.saturation_threshold) | |
| nonlin_term = -1j * n * (sat**2) * self.psi | |
| damping_term = -d * self.psi | |
| self.psi = self.psi + self.dt * (coupling_term + nonlin_term + damping_term) | |
| # --- Resonance from input signal --- | |
| # "Pluck" the center of the string | |
| self.psi[self.L // 2] += signal_in * 0.5 # Scale input | |
| # Stability checks | |
| self.psi = np.nan_to_num(self.psi, nan=0.0, posinf=0.0, neginf=0.0) | |
| amp_new = np.abs(self.psi) | |
| over = amp_new > self.max_amplitude_clip | |
| if np.any(over): | |
| self.psi[over] = self.psi[over] * (self.max_amplitude_clip / amp_new[over]) | |
| amp_now = np.abs(self.psi) | |
| # --- Knot Detection --- | |
| left = np.roll(amp_now, 1) | |
| right = np.roll(amp_now, -1) | |
| mask_thresh = amp_now > self.detect_threshold | |
| mask_local_max = (amp_now >= left) & (amp_now >= right) | |
| self.knots = np.where(mask_thresh & mask_local_max)[0] | |
| self.knot_count_out = len(self.knots) | |
| # --- Angular Momentum --- | |
| grad_psi = np.roll(self.psi, -1) - np.roll(self.psi, 1) | |
| moment_density = np.imag(np.conj(self.psi) * grad_psi) | |
| self.angular_momentum_out = float(np.sum(moment_density)) | |
| def get_output(self, port_name): | |
| if port_name == 'field_image': | |
| return self._draw_field_image(as_float=True) | |
| elif port_name == 'angular_momentum': | |
| return self.angular_momentum_out | |
| elif port_name == 'knot_count': | |
| return self.knot_count_out | |
| return None | |
| def _draw_field_image(self, as_float=False): | |
| h, w = 64, self.L | |
| img_color = np.zeros((h, w, 3), dtype=np.uint8) | |
| # Get field data | |
| amp_now = np.abs(self.psi) | |
| phase_now = np.angle(hilbert(self.psi.real)) | |
| # Normalize | |
| amp_norm = np.clip(amp_now / self.saturation_threshold, 0, 1) | |
| phase_norm = (phase_now + np.pi) / (2 * np.pi) | |
| # Draw amplitude (top half) and phase (bottom half) | |
| h_half = h // 2 | |
| for x in range(w): | |
| # Amplitude (Cyan) | |
| y_amp = int((h_half - 1) - amp_norm[x] * (h_half - 1)) | |
| img_color[y_amp, x] = (255, 255, 0) # BGR for Cyan | |
| # Phase (Magenta) | |
| y_phase = int(h_half + (h_half - 1) - phase_norm[x] * (h_half - 1)) | |
| img_color[y_phase, x] = (255, 0, 255) # BGR for Magenta | |
| # Draw center line | |
| cv2.line(img_color, (0, h // 2), (w, h // 2), (50, 50, 50), 1) | |
| # Draw knots (Red) | |
| for kx in self.knots: | |
| ky = int((h_half - 1) - amp_norm[kx] * (h_half - 1)) | |
| cv2.circle(img_color, (kx, ky), 3, (0, 0, 255), -1) # BGR for Red | |
| if as_float: | |
| return img_color.astype(np.float32) / 255.0 | |
| img_color = np.ascontiguousarray(img_color) | |
| return QtGui.QImage(img_color.data, w, h, 3*w, QtGui.QImage.Format.Format_BGR888) | |
| def get_display_image(self): | |
| return self._draw_field_image(as_float=False) | |
| def get_config_options(self): | |
| return [ | |
| ("Grid Size", "L", self.L, None), | |
| ("Knot Threshold", "detect_threshold", self.detect_threshold, None), | |
| ("Coupling", "coupling", self.coupling, None), | |
| ("Nonlinearity", "nonlinear", self.nonlinear, None), | |
| ("Damping", "damping", self.damping, None), | |
| ] |