diff --git "a/functions.py" "b/functions.py" --- "a/functions.py" +++ "b/functions.py" @@ -1,2082 +1,2087 @@ -# psi_solve2/functions.py - -import numpy as np -import matplotlib.pyplot as plt - -import math -from matplotlib.ticker import MultipleLocator - -# ========================================== -# 1. PHYSICS CONSTANTS -# ========================================== -hbar = 1 -m = 1 -L = 50 -N_GRID = 2000 -global Last_k_value # Used by harmonic() and check_harmonic_analytic() - -# ========================================== -# 2. GRID FUNCTIONS -# ========================================== -def make_grid(L=L, N=N_GRID): - """ - Create a spatial grid for solving the Schrödinger equation. - - Parameters - ---------- - L : float, optional - Total length of the spatial domain (default: 50 a.u.) - N : int, optional - Number of internal grid points (default: 2000) - - Returns - ------- - x_full : ndarray - Full grid with N+2 points from -L/2 to L/2, including boundary points - dx : float - Grid spacing (distance between adjacent points) - x_internal : ndarray - Internal grid points (N points) where the wavefunction is solved - Excludes the boundary points at x[0] and x[-1] - - Notes - ----- - The boundary points are used to enforce boundary conditions (typically ψ=0) - while x_internal contains the points where we actually solve for ψ. - - Examples - -------- - >>> x, dx, x_int = make_grid(L=20, N=1000) - >>> print(f"Domain: [{x[0]:.1f}, {x[-1]:.1f}], spacing: {dx:.4f}") - Domain: [-10.0, 10.0], spacing: 0.0200 - """ - x = np.linspace(-L/2, L/2, N+2) - dx = x[1] - x[0] - x_internal = x[1:-1] - return x, dx, x_internal - -# ========================================== -# 3. POTENTIAL GENERATORS (V(x)) -# ========================================== -def constant(x, c): - """ - Create a constant potential across the entire domain. - - Parameters - ---------- - x : ndarray - Spatial grid points - c : float - Constant potential value (in Hartree atomic units) - - Returns - ------- - V : ndarray - Constant potential array of same shape as x, with value c everywhere - - Examples - -------- - >>> x = np.linspace(-10, 10, 100) - >>> V = constant(x, 5.0) # V(x) = 5.0 everywhere - """ - return np.ones_like(x) * c - -def harmonic(x, k, center=0.0): - """ - Create a harmonic oscillator (parabolic) potential. - - Generates V(x) = (1/2)k(x - center)² representing a quantum harmonic - oscillator potential centered at the specified position. - - Parameters - ---------- - x : ndarray - Spatial grid points - k : float - Spring constant (curvature parameter) in atomic units - Larger k → stiffer spring → more tightly bound states - center : float, optional - Center position of the parabola (default: 0.0) - - Returns - ------- - V : ndarray - Harmonic potential array: V(x) = 0.5 * k * (x - center)² - - Notes - ----- - - Sets global variable Last_k_value for use by check_harmonic_analytic() - - Energy levels: E_n = ℏω(n + 1/2) where ω = √(k/m) - - In atomic units (ℏ=1, m=1): ω = √k - - Examples - -------- - >>> x = np.linspace(-10, 10, 1000) - >>> V = harmonic(x, k=1.0, center=0.0) # Standard QHO - >>> V_stiff = harmonic(x, k=10.0, center=0.0) # Stiffer spring - >>> V_offset = harmonic(x, k=1.0, center=5.0) # Centered at x=5 - """ - global Last_k_value - Last_k_value = k - - constant_factor = 1 - potential = 0.5 * k * (x - center)**2 - return constant_factor * potential - -def gaussian_well(x, center=0.0, width=1.0, depth=50): - """ - Create a Gaussian-shaped potential well. - - Generates a smooth, bell-shaped potential dip that can trap particles. - - Parameters - ---------- - x : ndarray - Spatial grid points - center : float, optional - Center position of the well (default: 0.0) - width : float, optional - Width parameter (standard deviation) of the Gaussian (default: 1.0) - Larger width → broader well - depth : float, optional - Depth of the well at the center (default: 50) - Positive depth creates a well (attractive potential) - - Returns - ------- - V : ndarray - Gaussian well potential: V(x) = -depth * exp(-(x-center)²/(2*width²)) - - Notes - ----- - - Minimum potential is -depth at x = center - - Potential approaches 0 as |x - center| → ∞ - - Smooth potential (infinitely differentiable) - - Examples - -------- - >>> x = np.linspace(-10, 10, 1000) - >>> V = gaussian_well(x, center=0, width=2.0, depth=10) - """ - return -depth * np.exp(-(x - center)**2 / (2 * width**2)) - -def inf_sqaure_well(x, lower_bound, upper_bound): - """ - Create an infinite square well (particle in a box) potential. - - Parameters - ---------- - x : ndarray - Spatial grid points - lower_bound : float - Left boundary of the well - upper_bound : float - Right boundary of the well - - Returns - ------- - V : ndarray - Infinite square well potential: - - V(x) = 0 for lower_bound ≤ x ≤ upper_bound (inside well) - - V(x) = 10¹⁰ for x < lower_bound or x > upper_bound (outside well) - - Notes - ----- - - Uses penalty method: "infinite" walls are approximated by very large - potential (10¹⁰) to enforce ψ ≈ 0 outside the well - - Well width: L = upper_bound - lower_bound - - Analytical energies: E_n = (ℏ²π²n²)/(2mL²) for n = 1, 2, 3, ... - - Examples - -------- - >>> x = np.linspace(-15, 15, 1000) - >>> V = inf_sqaure_well(x, lower_bound=-10, upper_bound=10) # L = 20 - >>> # Use with check_ISW_analytic(E, lower_bound=-10, upper_bound=10) - """ - HUGE_NUMBER = 1e10 - V = np.zeros_like(x) - V[x < lower_bound] = HUGE_NUMBER - V[x > upper_bound] = HUGE_NUMBER - return V - -def inf_wall(x, side, bound): - """ - Place an infinite potential wall on one side of the domain. - - Parameters - ---------- - x : ndarray - Spatial grid points - side : str - Which side to place the wall: 'left' or 'right' - (case-insensitive, strips whitespace and punctuation) - bound : float - Position of the wall boundary - - Returns - ------- - V : ndarray - Potential with infinite wall: - - If side='left': V(x) = 10¹⁰ for x < bound, V(x) = 0 for x ≥ bound - - If side='right': V(x) = 10¹⁰ for x > bound, V(x) = 0 for x ≤ bound - - Notes - ----- - Uses penalty method with V = 9×10¹⁰ to approximate infinite potential. - - Examples - -------- - >>> x = np.linspace(-10, 10, 1000) - >>> V_left = inf_wall(x, 'left', bound=-5) # Wall at x=-5, blocks left side - >>> V_right = inf_wall(x, 'right', bound=5) # Wall at x=5, blocks right side - """ - V = np.zeros_like(x) - HUGE_NUMBER = 9e10 - side = side.strip(', . ').lower() - - if side == 'left': - V[x < bound] = HUGE_NUMBER - elif side == 'right': - V[x > bound] = HUGE_NUMBER - return V - -def finite_barrier(x, center, width, height): - """ - Create a finite rectangular potential barrier. - - Parameters - ---------- - x : ndarray - Spatial grid points - center : float - Center position of the barrier - width : float - Total width of the barrier - height : float - Height of the potential barrier - - Returns - ------- - V : ndarray - Rectangular barrier potential: - - V(x) = height for |x - center| < width/2 - - V(x) = 0 elsewhere - - Notes - ----- - Useful for studying quantum tunneling phenomena. Particles with E < height - can tunnel through the barrier with exponentially decaying probability. - - Examples - -------- - >>> x = np.linspace(-10, 10, 1000) - >>> V = finite_barrier(x, center=0, width=2, height=5) # Barrier from x=-1 to x=1 - """ - V = np.zeros_like(x) - mask = (x > (center - width/2)) & (x < (center + width/2)) - V[mask] = height - return V - -def V_double_well(x, depth=20, separation=1, center=0.0): - """ - Create a quartic double-well potential. - - Generates V(x) = depth × ((x-center)² - separation)² which has two minima - separated by a central barrier. - - Parameters - ---------- - x : ndarray - Spatial grid points - depth : float, optional - Depth parameter controlling overall potential strength (default: 20) - separation : float, optional - Controls the distance between the two wells (default: 1) - Well minima are approximately at x = center ± separation - center : float, optional - Center position of the double well system (default: 0.0) - - Returns - ------- - V : ndarray - Double well potential: V(x) = depth × ((x-center)² - separation)² - - Notes - ----- - - Creates symmetric double well with barrier at x = center - - Useful for studying tunneling splitting and symmetric/antisymmetric states - - Ground state and first excited state form tunneling doublet - - Examples - -------- - >>> x = np.linspace(-5, 5, 1000) - >>> V = V_double_well(x, depth=2, separation=1, center=0) - """ - V = depth * ((x - center)**2 - separation)**2 - return V - -def custom2(value,x): - """Helper function from the notebook.""" - return value * np.ones_like(x) - -# In psi_solve2/functions.py - -def finite_square_well(x, lower_bound, upper_bound, depth_V): - """ - Create a finite square well potential. - - The potential is zero inside the well and has finite height depth_V outside. - Unlike the infinite square well, particles can exist in the barrier region - with exponentially decaying wavefunctions. - - Parameters - ---------- - x : ndarray - Spatial grid points - lower_bound : float - Left boundary of the well - upper_bound : float - Right boundary of the well - depth_V : float - Height of the potential barriers outside the well (V₀) - - Returns - ------- - V : ndarray - Finite square well potential: - - V(x) = 0 for lower_bound ≤ x ≤ upper_bound (inside well) - - V(x) = depth_V for x < lower_bound or x > upper_bound (barrier regions) - - """ - """ - Notes - ----- - - Bound states exist only when E < depth_V - - Number of bound states depends on well width and depth_V - - For bound states, wavefunction decays exponentially in barrier (E < V) - - For scattering states (E > depth_V), wavefunction oscillates everywhere - - Use check_finite_well_analytic() to verify numerical results - - Examples - -------- - >>> x = np.linspace(-15, 15, 1000) - >>> V_deep = finite_square_well(x, -10, 10, depth_V=2.0) # Deep well, many bound states - >>> V_shallow = finite_square_well(x, -10, 10, depth_V=0.01) # Shallow, few/no bound states - """ - # Start with a baseline of zero potential - V = np.zeros_like(x) - - # The walls outside the well are set to the height/depth V_0 - V[x < lower_bound] = depth_V - V[x > upper_bound] = depth_V - - # The potential *inside* the well remains V=0 (or whatever you set the baseline to) - return V - -# ========================================== -# 4. SCHRÖDINGER EQUATION SOLVER -# ========================================== -def kinetic_operator(N, dx, hbar=hbar, m=m): - """ - Build the kinetic energy operator matrix using finite difference method. - - Constructs the discrete representation of the kinetic energy operator - T = -(ℏ²/2m) d²/dx² using a 3-point central difference stencil. - - Parameters - ---------- - N : int - Number of internal grid points (size of the matrix) - dx : float - Grid spacing (distance between adjacent points) - hbar : float, optional - Reduced Planck constant (default: 1.0 in atomic units) - m : float, optional - Particle mass (default: 1.0 in atomic units) - - Returns - ------- - T : ndarray, shape (N, N) - Kinetic energy operator matrix (symmetric, tridiagonal) - - Diagonal elements: -(ℏ²/2m) × (-2/dx²) - - Off-diagonal elements: -(ℏ²/2m) × (1/dx²) - - """ - """ - Notes - ----- - The second derivative is approximated using central differences: - d²ψ/dx² ≈ (ψ_{i+1} - 2ψ_i + ψ_{i-1}) / dx² - - This creates a tridiagonal matrix: - - Main diagonal: -2/dx² - - Upper/lower diagonals: +1/dx² - - The kinetic energy operator is then: T = -(ℏ²/2m) × D2 - - Examples - -------- - >>> N = 1000 - >>> dx = 0.025 - >>> T = kinetic_operator(N, dx) - >>> print(f"Matrix shape: {T.shape}, Symmetric: {np.allclose(T, T.T)}") - Matrix shape: (1000, 1000), Symmetric: True - """ - main_diagonal = (1/dx**2) * np.diag(-2 * np.ones(N)) - off_diagonal1 = (1/dx**2) * np.diag(np.ones(N-1), -1) - off_diagonal2 = (1/dx**2) * np.diag(np.ones(N-1), 1) - D2 = (main_diagonal + off_diagonal1 + off_diagonal2) - - T = (-(hbar**2 / (2*m)) * D2) - return T - -def solve(T, V_full, dx): - """ - Solve the time-independent Schrödinger equation for eigenvalues and eigenvectors. - - Solves the eigenvalue problem Hψ = Eψ where H = T + V is the Hamiltonian. - Returns normalized eigenstates sorted by energy. - - Parameters - ---------- - T : ndarray, shape (N, N) - Kinetic energy operator matrix from kinetic_operator() - V_full : ndarray, shape (N+2,) - Full potential array including boundary points - V_full[0] and V_full[-1] are boundary values (typically very large) - V_full[1:-1] are the internal potential values - dx : float - Grid spacing used for normalization - - Returns - ------- - E : ndarray, shape (N,) - Eigenvalues (energy levels) sorted in ascending order - Units: Hartree (atomic units) - psi : ndarray, shape (N, N) - Eigenvectors (wavefunctions) as columns - psi[:, i] is the wavefunction for energy E[i] - Each wavefunction is normalized: ∫|ψ|² dx = 1 - - """ - """ - Notes - ----- - - Uses np.linalg.eigh() which assumes Hermitian matrix (guaranteed for H) - - Automatically sorts eigenvalues and eigenvectors by energy - - Normalizes each eigenstate using trapezoidal rule: ∫|ψ|² dx = 1 - - Boundary conditions are enforced by V_full having large values at edges - - The Hamiltonian is constructed as: - H = T + diag(V_internal) - where V_internal = V_full[1:-1] - - Examples - -------- - >>> # Setup - >>> x, dx, x_int = make_grid(L=20, N=1000) - >>> T = kinetic_operator(len(x_int), dx) - >>> - >>> # Create infinite square well - >>> V = inf_sqaure_well(x_int, -10, 10) - >>> V_full = np.pad(V, (1,1), constant_values=1e10) - >>> - >>> # Solve - >>> E, psi = solve(T, V_full, dx) - >>> print(f"Ground state energy: {E[0]:.6f} Ha") - >>> - >>> # Verify normalization - >>> norm = np.sum(psi[:, 0]**2) * dx - >>> print(f"Normalization: {norm:.6f}") # Should be 1.0 - """ - V_internal = V_full[1:-1] - H = T + np.diag(V_internal) - - E, psi = np.linalg.eigh(H) - - # Normalize each state individually - for i in range(psi.shape[1]): - # sum( |psi|^2 * dx ) - norm_factor = np.sum(psi[:, i]**2) * dx - # Divide by the square root of the integral - psi[:, i] = psi[:, i] / np.sqrt(norm_factor) - - return E, psi - -# ========================================== -# 5. PLOTTING FUNCTIONS (STREAMLIT/JUPYTER SAFE) -# ========================================== -def plot_V(V_raw_input): - """ - Plot a 1D potential profile. - - Creates a simple matplotlib figure showing the potential energy landscape. - - Parameters - ---------- - V_raw_input : ndarray or None - 1D array representing the potential V(x) - If None or scalar, returns None - - Returns - ------- - fig : matplotlib.figure.Figure or None - Figure object containing the potential plot - Returns None if input is invalid - """ - """ - Notes - ----- - - Uses dark background style - - Cyan color for potential curve - - Useful for quick visualization of potential shapes - - Examples - -------- - >>> x = np.linspace(-10, 10, 1000) - >>> V = harmonic(x, k=1.0) - >>> fig = plot_V(V) - >>> plt.show() - """ - if V_raw_input is None or np.ndim(V_raw_input) == 0: - return None - - plt.style.use("dark_background") - fig, ax = plt.subplots(figsize=(6, 2)) - ax.plot(V_raw_input, lw=1.5, color="cyan") - ax.set_title("Potential Input") - ax.set_xlabel("Grid index") - ax.set_ylabel("Potential") - fig.tight_layout() - return fig - - -def plot_alive(E, psi, V, x, no=1, nos=5, mode=''): - """ - Plot wavefunctions as probability densities with separate energy and probability axes. - - Creates a physically accurate plot showing: - - Potential V(x) and energy levels on left y-axis - - Probability densities |ψ|² on right y-axis (separate scale) - - Color-synchronized between probability curves and energy levels - - Parameters - ---------- - E : ndarray - Energy eigenvalues (in Hartree) - psi : ndarray, shape (N, M) - Wavefunction array where psi[:, i] is the i-th eigenstate - V : ndarray, shape (N+2,) - Full potential array including boundaries - x : ndarray, shape (N+2,) - Full spatial grid including boundaries - no : int, optional - State index to plot if mode != 'all' (default: 1) - nos : int, optional - Number of states to plot if mode == 'all' (default: 5) - mode : str, optional - Plot mode: - - 'all': Plot multiple states (first nos states) - - '': Plot single state (state no) - Default: '' (single state) - - Returns - ------- - fig : matplotlib.figure.Figure - Figure object with dual y-axes - - ax1 (left): Energy/Potential scale - - ax2 (right): Probability density scale - - Notes - ----- - - Uses dark background theme - - Probability densities are plotted as |ψ|², not ψ - - Each state has matching colors for its probability curve and energy level - - Regions where V > 10⁵ are hidden (infinite walls) - - """ - """ - Examples - -------- - >>> # Plot first 5 states - >>> fig = plot_alive(E, psi, V_full, x_full, nos=5, mode='all') - >>> plt.show() - >>> - >>> # Plot only ground state - >>> fig = plot_alive(E, psi, V_full, x_full, no=0) - >>> plt.show() - """ - import matplotlib.pyplot as plt - - plt.style.use("dark_background") - fig, ax1 = plt.subplots(figsize=(10, 6)) - - ax2 = ax1.twinx() # Right axis for probability - - states = min(nos, len(E)) - x_solver = x[1:-1] - V_internal = V[1:-1] - - # --- Plot Potential --- - ax1.plot(x, V, color="white", lw=2, label="V(x)", alpha=0.7) - - # --- Plot wavefunctions --- - if mode == 'all': - for n in range(states): - # 1. Get the synchronized color for this state - color = plt.colormaps["tab20"].colors[n % 20] - - psi_n_sq = psi[:, n]**2 - - # 2. Plot probability density on ax2 with the chosen color - ax2.plot( - x_solver, psi_n_sq, - label=rf"$|\psi_{n}|^2$ (E={E[n]:.2f})", - lw=1.2, - color=color # <-- EXPLICIT COLOR SET - ) - - # 3. Plot energy line on ax1 with the same color - ax1.axhline(E[n], linestyle="--", lw=0.8, alpha=0.5, color=color) # <-- EXPLICIT COLOR SET - else: - # For single state mode, we still need a color. Use 'no' as the index. - n = no - color = plt.colormaps["tab20"].colors[n % 20] - - psi_n_sq = psi[:, n]**2 - - # Plot probability density on ax2 with the chosen color - ax2.plot( - x_solver, psi_n_sq, - label=rf"$|\psi_{no}|^2$ (E={E[no]:.2f})", - lw=1.2, - color=color # <-- EXPLICIT COLOR SET - ) - - # Plot energy line on ax1 with the same color - ax1.axhline(E[no], linestyle="--", lw=0.8, alpha=0.5, color=color) # <-- EXPLICIT COLOR SET - - # Formatting - ax1.set_xlabel("x [a.u.]") - ax1.set_ylabel("Energy / V(x)") - ax2.set_ylabel(r"Probability Density $|\psi|^2$") - - ax1.set_title("Physically Accurate Eigenstates and Potential") - - # The fig.legend() might show the color/style of the *last* plot line. - # For robust legend handling with twinx, you often need to combine the handles. - h1, l1 = ax1.get_legend_handles_labels() - h2, l2 = ax2.get_legend_handles_labels() - ax1.legend(h1+h2, l1+l2, loc="upper right", fontsize=8) # <-- Improved Legend - - plt.tight_layout() - return fig - -def plot_dead(E, psi, V, x, nos=5): - """Textbook: wavefunctions vertically shifted by energy.""" - plt.style.use("dark_background") - fig, (ax_main, ax_bar) = plt.subplots( - 1, 2, figsize=(10, 7), gridspec_kw={"width_ratios": [5, 1]} - ) - fig.subplots_adjust(bottom=0.2, wspace=0.4) - - states = min(nos, len(E)) - x_solver = x[1:-1] - V_internal = V[1:-1] - - if states <= 0: - return fig - - scale = (E[1] - E[0]) * 0.4 if states > 1 else max(E[0] * 0.1, 0.5) - max_E = E[states - 1] - window_height = max_E * 1.5 - - # Plot shifted wavefunctions - for n in range(states): - psi_n = psi[:, n] - maxabs = np.max(np.abs(psi_n)) - psi_norm = psi_n / (maxabs if maxabs != 0 else 1) - y = psi_norm * scale + E[n] - y[V_internal > 1e5] = np.nan # hide where potential is infinite - - color = plt.colormaps["tab20"].colors[n % 20] - ax_main.plot(x_solver, y, lw=1.3, color=color, label=f"n={n+1}, E={E[n]:.2f}") - - # Plot potential - V_clip = np.clip(V, 0, window_height) - ax_main.plot(x, V_clip, color="white", lw=2, label="V(x)") - - ax_main.set_title("Eigenstates + Potential") - ax_main.set_xlabel("x [a.u.]") - ax_main.set_ylabel("Energy / ψ") - ax_main.set_ylim(0, max_E * 1.2) - ax_main.legend(fontsize=8) - - # Energy levels - ax_bar.set_title("Energy Spectrum") - ax_bar.set_xticks([]) - ax_bar.set_ylim(0, np.max(E[:states]) * 1.1) - for n in range(states): - ax_bar.axhline(E[n], lw=1, color=plt.colormaps["tab20"].colors[n % 20]) - - return fig - - -# ========================================== -# 6. BENCHMARKING FUNCTIONS -# ========================================== -def check_ortho(psi, dx, num_states_to_check=20): - """ - Checks the orthonormality of the first 'num_states_to_check' wave functions. - """ - N_CHECK = min(psi.shape[1], num_states_to_check) - overlap_matrix = np.zeros((N_CHECK, N_CHECK)) - - for i in range(N_CHECK): - for j in range(N_CHECK): - # Riemann Sum: integral(psi_i * psi_j) dx - Rsum = np.sum(psi[:, i] * psi[:, j]) * dx - overlap_matrix[i, j] = Rsum - - print(f"\n--- Orthonormality Check (First {N_CHECK} states) ---") - print("Overlap Matrix should approximate the Identity Matrix:") - return overlap_matrix - -def show_matrix(overlap_matrix,how='normal',round_value=10): - ''' - how = normal, round , plot - ''' - if how == 'normal': - print(overlap_matrix[:3]) - elif how == 'round': - print(np.round(overlap_matrix, round_value)) - elif how == 'plot': - plt.figure(figsize=(6,5)) - plt.imshow(overlap_matrix, cmap='coolwarm', origin='lower') - plt.colorbar(label="Overlap Value") - plt.title("Orthonormality Check Matrix") - plt.xlabel("State Index m") - plt.ylabel("State Index n") - plt.gca().invert_yaxis() - plt.locator_params(axis='y', integer=True) - plt.locator_params(axis='x', integer=True) - plt.show() - -def check_ISW_analytic(E, lower_bound=-10, upper_bound=10, hbar=1.0, m=1.0, max_levels=6): - """ - Compares numerical energies to the Infinite Square Well analytic formula. - - Parameters: - ----------- - E : array - Numerical eigenvalues - lower_bound : float - Lower boundary of the well (default: -10) - upper_bound : float - Upper boundary of the well (default: 10) - hbar : float - Reduced Planck constant (default: 1.0) - m : float - Particle mass (default: 1.0) - max_levels : int - Number of levels to check (default: 6) - - """ - """ - Example: - -------- - check_ISW_analytic(E, lower_bound=-10, upper_bound=10) - """ - L = upper_bound - lower_bound # Well width - CHECK_N = min(max_levels, len(E)) - E_numerical = E[:CHECK_N] - E_analytic = np.zeros(CHECK_N) - - for i in range(CHECK_N): - n = i + 1 - E_analytic[i] = (hbar**2 * np.pi**2 * n**2) / (2*m*L**2) - - print("\n### ENERGY BENCHMARK: Infinite Square Well ###") - print(f"Well boundaries: x = [{lower_bound}, {upper_bound}], Width L = {L}") - print("-" * 55) - print(f"| n | Analytic E | Numerical E | % Error |") - print("-" * 55) - - for i in range(CHECK_N): - percent_error = np.abs((E_numerical[i] - E_analytic[i]) / E_analytic[i]) * 100 - print( - f"| {i+1:<1} | {E_analytic[i]:<10.6f} | {E_numerical[i]:<11.6f} | {percent_error:<7.4f}% |" - ) - print("-" * 55) - - return E_analytic, E_numerical - -def check_harmonic_analytic(E, k=None, center=0.0, hbar=1.0, m=1.0, max_levels=6): - """ - Compares numerical energies to the Harmonic Oscillator analytic formula. - - Parameters: - ----------- - E : array - Numerical eigenvalues - k : float, optional - Spring constant. If None, uses Last_k_value global variable - center : float - Center position of the harmonic oscillator (default: 0.0) - hbar : float - Reduced Planck constant (default: 1.0) - m : float - Particle mass (default: 1.0) - max_levels : int - Number of levels to check (default: 6) - - Example: - -------- - check_harmonic_analytic(E, k=10, center=0) - """ - CHECK_N = min(max_levels, len(E)) - - try: - # Use provided k or fall back to global Last_k_value - if k is None: - k = Last_k_value - if k is None: - print("ERROR: k is not set. Please provide k parameter or run harmonic() first.") - return - - w = np.sqrt(k/m) - E_numerical = E[:CHECK_N] - E_analytic = np.zeros(CHECK_N) - - for i in range(CHECK_N): - n_quantum = i - E_analytic[i] = (n_quantum + 0.5) * hbar * w - - print("\n### ENERGY BENCHMARK: Harmonic Oscillator ###") - print(f"Spring constant k = {k}, Center = {center}, omega = {w:.4f}") - print("-" * 55) - print(f"| n | Analytic E | Numerical E | % Error |") - print("-" * 55) - - for i in range(CHECK_N): - n_label = i - percent_error = np.abs((E_numerical[i] - E_analytic[i]) / E_analytic[i]) * 100 - - print( - f"| {n_label:<1} | {E_analytic[i]:<10.6f} | {E_numerical[i]:<11.6f} | {percent_error:<7.4f}% |" - ) - print("-" * 55) - - return E_analytic, E_numerical - - except Exception as e: - print(f"Error in harmonic oscillator check: {e}") - - -def check_finite_well_analytic(E, V0, lower_bound=-10, upper_bound=10, hbar=1.0, m=1.0, max_levels=10): - """ - Compares numerical energies to the Finite Square Well analytical solution. - - The finite square well has no simple closed-form solution, but bound state - energies can be found by solving transcendental equations numerically. - - Parameters: - ----------- - E : array - Numerical eigenvalues from your solver - V0 : float - Barrier height (potential outside the well) - lower_bound : float - Lower boundary of the well (default: -10) - upper_bound : float - Upper boundary of the well (default: 10) - hbar : float - Reduced Planck constant (default: 1.0) - m : float - Particle mass (default: 1.0) - max_levels : int - Maximum number of levels to check (default: 10) - - Example: - -------- - check_finite_well_analytic(E, V0=2.0, lower_bound=-10, upper_bound=10) - """ - a = (upper_bound - lower_bound) / 2 # Half-width - z0 = a * np.sqrt(2 * m * V0) / hbar # Dimensionless parameter - - # Find analytical energies by solving transcendental equations - E_analytic = [] - - # Even parity states: z*tan(z) = sqrt(z0^2 - z^2) - z_vals = np.linspace(0.01, z0 - 0.01, 10000) - for n in range(max_levels): - try: - lhs = z_vals * np.tan(z_vals) - rhs = np.sqrt(z0**2 - z_vals**2) - diff = lhs - rhs - - # Find sign changes (crossings) - for i in range(len(diff) - 1): - if diff[i] * diff[i+1] < 0: - z = z_vals[i] - E_candidate = (hbar**2 * z**2) / (2 * m * a**2) - if E_candidate < V0 and not any(np.isclose(E_candidate, E_a, rtol=1e-3) for E_a in E_analytic): - E_analytic.append(E_candidate) - break - except: - pass - - # Odd parity states: -z*cot(z) = sqrt(z0^2 - z^2) - for n in range(max_levels): - try: - lhs = -z_vals / np.tan(z_vals) - rhs = np.sqrt(z0**2 - z_vals**2) - diff = lhs - rhs - - for i in range(len(diff) - 1): - if diff[i] * diff[i+1] < 0: - z = z_vals[i] - E_candidate = (hbar**2 * z**2) / (2 * m * a**2) - if E_candidate < V0 and not any(np.isclose(E_candidate, E_a, rtol=1e-3) for E_a in E_analytic): - E_analytic.append(E_candidate) - break - except: - pass - - E_analytic = sorted(E_analytic) - - # Filter numerical energies to only bound states - E_numerical_bound = E[E < V0] - - CHECK_N = min(len(E_analytic), len(E_numerical_bound), max_levels) - - if CHECK_N == 0: - print("\n### ENERGY BENCHMARK: Finite Square Well ###") - print(f"Well: x in [{lower_bound}, {upper_bound}], V0 = {V0}, z0 = {z0:.4f}") - print("WARNING: No bound states found!") - print(f" Barrier too shallow. Need V0 > {E[0]:.4f} to bind the ground state.") - return None, None - - print("\n### ENERGY BENCHMARK: Finite Square Well ###") - print(f"Well: x in [{lower_bound}, {upper_bound}], V0 = {V0}, z0 = {z0:.4f}") - print(f"Number of bound states: {CHECK_N}") - print("-" * 55) - print(f"| n | Analytic E | Numerical E | % Error |") - print("-" * 55) - - for i in range(CHECK_N): - percent_error = np.abs((E_numerical_bound[i] - E_analytic[i]) / E_analytic[i]) * 100 - print( - f"| {i:<1} | {E_analytic[i]:<10.6f} | {E_numerical_bound[i]:<11.6f} | {percent_error:<7.4f}% |" - ) - print("-" * 55) - - return np.array(E_analytic[:CHECK_N]), E_numerical_bound[:CHECK_N] - - - - -## -# Verify - -import sys - -def run_comparison(): - """ - Cross-verification: Hand-wave solver vs QMSolve package. - - Compares results for: - 1. Double Well potential - 2. Harmonic Oscillator (debug test) - - Results saved to 'comparison_log.txt' - - Requires - -------- - QMSolve package: pip install qmsolve - - Usage - ----- - >>> from functions import run_comparison - >>> run_comparison() - """ - # Import qmsolve only when this function is called - try: - from qmsolve import Hamiltonian, SingleParticle, init_visualization - except ImportError: - print("Error: qmsolve not found. Please install it via 'pip install qmsolve'") - return - - with open("comparison_log.txt", "w") as log_file: - sys.stdout = log_file - print("========================================") - print("CROSS-VERIFICATION: Hand-wave vs QMSOLVE") - print("========================================") - - # --------------------------------------------------------- - # CASE: Double Well Potential - # V(x) = depth * ( (x-center)**2 - separation )**2 - # --------------------------------------------------------- - print("\n[TEST CASE] Double Well Potential") - - # Parameters - L = 10.0 - N = 512 # QMSolve default is often 512 or similar, let's match - depth = 2.0 - separation = 1.0 - center = 0.0 - m_particle = 1.0 - - print(f"Parameters: L={L}, N={N}, depth={depth}, separation={separation}, m={m_particle}") - - # --------------------------------------------------------- - # 1. Run Hand-wave solver - # --------------------------------------------------------- - print("\n--- Running Hand-wave Solver ---") - x_full, dx, x_internal = make_grid(L=L, N=N) - - # Construct Potential using local V_double_well function - V_internal = V_double_well(x_internal, depth=depth, separation=separation, center=center) - - # Pad for solver - V_full = np.zeros_like(x_full) - V_full[1:-1] = V_internal - V_full[0] = 1e10 - V_full[-1] = 1e10 - - T = kinetic_operator(N, dx, m=m_particle) - E_handwave, psi_handwave = solve(T, V_full, dx) - - print(f"Hand-wave Energies (first 5): {E_handwave[:5]}") - - # --------------------------------------------------------- - # 2. Run QMSolve - # --------------------------------------------------------- - print("\n--- Running QMSolve ---") - - # Define potential function for QMSolve - def double_well(particle): - x = particle.x - return depth * ( (x - center)**2 - separation )**2 - - # Setup QMSolve - H = Hamiltonian(particles = SingleParticle(m = m_particle), - potential = double_well, - spatial_ndim = 1, N = N, extent = L) - - # Diagonalize - eigenstates = H.solve(max_states = 10) - E_qm_eV = eigenstates.energies - - # Convert QMSolve (eV) to Hartree - # 1 Hartree = 27.211386 eV - Hartree_to_eV = 27.211386 - E_qm = E_qm_eV / Hartree_to_eV - - print(f"QMSolve Energies (eV): {E_qm_eV[:5]}") - print(f"QMSolve Energies (Hartree): {E_qm[:5]}") - - # --------------------------------------------------------- - # 3. Compare - # --------------------------------------------------------- - print("\n--- Comparison Results ---") - print("-" * 65) - print(f"| n | Hand-wave E | QMSolve E | Diff | % Diff |") - print("-" * 65) - - for i in range(5): - e1 = E_handwave[i] - e2 = E_qm[i] - diff = abs(e1 - e2) - p_diff = (diff / e2) * 100 if e2 != 0 else 0.0 - - print(f"| {i:<1} | {e1:<12.6f} | {e2:<12.6f} | {diff:<12.2e} | {p_diff:<7.4f}% |") - print("-" * 65) - - # --------------------------------------------------------- - # DEBUG CASE: Harmonic Oscillator - # --------------------------------------------------------- - print("\n[DEBUG CASE] Harmonic Oscillator (k=1)") - k_debug = 1.0 - - # Hand-wave solver - V_internal_HO = 0.5 * k_debug * x_internal**2 - V_full_HO = np.zeros_like(x_full) - V_full_HO[1:-1] = V_internal_HO - V_full_HO[0] = 1e10 - V_full_HO[-1] = 1e10 - - E_handwave_HO, _ = solve(T, V_full_HO, dx) - print(f"Hand-wave HO Energies: {E_handwave_HO[:5]}") - - # QMSolve - def harmonic_potential(particle): - return 0.5 * k_debug * particle.x**2 - - H_HO = Hamiltonian(particles = SingleParticle(m = m_particle), - potential = harmonic_potential, - spatial_ndim = 1, N = N, extent = L) - eigenstates_HO = H_HO.solve(max_states = 10) - E_qm_HO = eigenstates_HO.energies - print(f"QMSolve HO Energies: {E_qm_HO[:5]}") - - sys.stdout = sys.__stdout__ - print("\n✓ Comparison complete! Results saved to 'comparison_log.txt'") - - -# ========================================== -# NOTEBOOK-FRIENDLY VERIFICATION FUNCTIONS -# ========================================== - -def verify_qmsolve(E_your=None, psi_your=None, V_your=None, x_your=None, - potential_type='double_well', potential_params=None): - """ - QMSolve comparison using YOUR notebook variables. - - Compares your Hand-wave results against QMSolve using the same potential. - - Parameters - ---------- - E_your : ndarray, optional - Your computed energy eigenvalues - If None, will compute using default double well - psi_your : ndarray, optional - Your computed wavefunctions - V_your : ndarray, optional - Your potential array (full, including boundaries) - x_your : ndarray, optional - Your spatial grid (full, including boundaries) - potential_type : str, optional - Type of potential: 'double_well', 'harmonic', 'custom' - Default: 'double_well' - potential_params : dict, optional - Parameters for the potential, e.g.: - {'depth': 2.0, 'separation': 1.0, 'center': 0.0} for double_well - {'k': 1.0, 'center': 0.0} for harmonic - - Usage in notebook - ----------------- - # After you've computed E, psi, V, x in your notebook: - >>> verify_qmsolve(E_your=E, psi_your=psi, V_your=V_full, x_your=x, - ... potential_type='double_well', - ... potential_params={'depth': 2.0, 'separation': 1.0, 'center': 0.0}) - - # Or use defaults: - >>> verify_qmsolve() - """ - try: - from qmsolve import Hamiltonian, SingleParticle - except ImportError: - print("❌ Error: qmsolve not found.") - print("Install with: pip install qmsolve") - return - - print("="*70) - print("CROSS-VERIFICATION: Your Results vs QMSolve") - print("="*70) - - # Use provided values or compute defaults - if E_your is None or x_your is None: - print("\n⚠️ No input provided. Using default Double Well test case.") - - # Default parameters - L = 10.0 - N = 512 - if potential_params is None: - potential_params = {'depth': 2.0, 'separation': 1.0, 'center': 0.0} - - print(f"\n[TEST] {potential_type.replace('_', ' ').title()}") - print(f"Parameters: L={L}, N={N}, {potential_params}") - - # Compute using Hand-wave - x_your, dx, x_internal = make_grid(L=L, N=N) - - if potential_type == 'double_well': - V_internal = V_double_well(x_internal, **potential_params) - elif potential_type == 'harmonic': - V_internal = harmonic(x_internal, **potential_params) - else: - print("❌ Unknown potential type") - return - - V_your = np.zeros_like(x_your) - V_your[1:-1] = V_internal - V_your[0] = 1e10 - V_your[-1] = 1e10 - - T = kinetic_operator(N, dx) - E_your, psi_your = solve(T, V_your, dx) - else: - # Use provided values - print(f"\n✓ Using your computed results") - print(f" Grid points: {len(x_your)}") - print(f" Domain: [{x_your[0]:.2f}, {x_your[-1]:.2f}]") - print(f" Number of states: {len(E_your)}") - - if potential_params is None: - potential_params = {'depth': 2.0, 'separation': 1.0, 'center': 0.0} - - L = x_your[-1] - x_your[0] - N = len(x_your) - 2 # Internal points - - print(f"\n--- Your Hand-wave Results ---") - print(f"Energies (first 5): {E_your[:5]}") - - # Run QMSolve with same parameters - print(f"\n--- Running QMSolve with same potential ---") - - # Define potential function for QMSolve - if potential_type == 'double_well': - depth = potential_params.get('depth', 2.0) - separation = potential_params.get('separation', 1.0) - center = potential_params.get('center', 0.0) - - def potential_func(particle): - x = particle.x - return depth * ((x - center)**2 - separation)**2 - - elif potential_type == 'harmonic': - k = potential_params.get('k', 1.0) - center = potential_params.get('center', 0.0) - - def potential_func(particle): - return 0.5 * k * (particle.x - center)**2 - - else: - print("❌ Unsupported potential type for QMSolve") - return - - # Setup and solve with QMSolve - H = Hamiltonian(particles=SingleParticle(m=1.0), - potential=potential_func, - spatial_ndim=1, N=N, extent=L) - - eigenstates = H.solve(max_states=min(10, len(E_your))) - E_qm_eV = eigenstates.energies - - # Convert to Hartree - Hartree_to_eV = 27.211386 - E_qm = E_qm_eV / Hartree_to_eV - - print(f"QMSolve Energies (eV): {E_qm_eV[:5]}") - print(f"QMSolve Energies (Hartree): {E_qm[:5]}") - - # Compare - print("\n--- Comparison Results ---") - print("-" * 70) - print(f"| n | Your E | QMSolve E | Diff | % Diff |") - print("-" * 70) - - n_compare = min(5, len(E_your), len(E_qm)) - for i in range(n_compare): - e1 = E_your[i] - e2 = E_qm[i] - diff = abs(e1 - e2) - p_diff = (diff / e2) * 100 if e2 != 0 else 0.0 - print(f"| {i:<1} | {e1:<12.6f} | {e2:<12.6f} | {diff:<12.2e} | {p_diff:<7.4f}% |") - - print("-" * 70) - - # Summary - avg_diff = np.mean([abs(E_your[i] - E_qm[i])/E_qm[i]*100 for i in range(n_compare)]) - max_diff = np.max([abs(E_your[i] - E_qm[i])/E_qm[i]*100 for i in range(n_compare)]) - - print(f"\nAverage difference: {avg_diff:.4f}%") - print(f"Maximum difference: {max_diff:.4f}%") - - if max_diff < 0.5: - print("✅ EXCELLENT: Your solver matches QMSolve within 0.5%!") - elif max_diff < 1.0: - print("✅ GOOD: Your solver matches QMSolve within 1%") - else: - print("⚠️ WARNING: Difference > 1%. Check your implementation.") - - print("\n✅ QMSolve verification complete!") - - -def verify_physics(): - """ - Comprehensive physics tests that print directly (no file output). - - Tests: - 1. Infinite Square Well - 2. Harmonic Oscillator - 3. Orthonormality - - Usage in notebook: - >>> from functions import verify_physics - >>> verify_physics() - """ - print("="*70) - print("PHYSICS VERIFICATION") - print("="*70) - - # Test 1: Infinite Square Well - print("\n[TEST 1] Infinite Square Well") - print("-"*70) - L = 20.0 - N = 1000 - x_full, dx, x_internal = make_grid(L=L, N=N) - - V_full = np.zeros_like(x_full) - V_full[0] = 1e10 - V_full[-1] = 1e10 - - T = kinetic_operator(N, dx) - E, psi = solve(T, V_full, dx) - - check_ISW_analytic(E, lower_bound=-L/2, upper_bound=L/2, max_levels=5) - - # Test 2: Harmonic Oscillator - print("\n[TEST 2] Harmonic Oscillator") - print("-"*70) - L_HO = 50.0 - N_HO = 2000 - x_full, dx, x_internal = make_grid(L=L_HO, N=N_HO) - - k = 1.0 - V_internal = harmonic(x_internal, k=k) - - V_full = np.zeros_like(x_full) - V_full[1:-1] = V_internal - V_full[0] = 1e10 - V_full[-1] = 1e10 - - T = kinetic_operator(N_HO, dx) - E, psi = solve(T, V_full, dx) - - check_harmonic_analytic(E, k=k, max_levels=5) - - # Test 3: Orthonormality - print("\n[TEST 3] Orthonormality") - print("-"*70) - overlap = check_ortho(psi, dx, num_states_to_check=5) - - max_off_diag = np.max(np.abs(overlap - np.eye(len(overlap)))) - print(f"Max off-diagonal element: {max_off_diag:.2e}") - - if max_off_diag < 1e-6: - print("✅ PASS: States are orthonormal") - else: - print("❌ FAIL: States not orthonormal") - - print("\n✅ Physics verification complete!") - - -def verify_all(): - """ - Run all verifications (prints directly, no files). - - Usage in notebook: - >>> from functions import verify_all - >>> verify_all() - """ - print("\n" + "="*70) - print("COMPLETE SOLVER VALIDATION") - print("="*70) - - # Run physics tests - verify_physics() - - print("\n") - - # Run QMSolve comparison - verify_qmsolve() - - print("\n" + "="*70) - print("✅ ALL VALIDATIONS COMPLETE!") - print("="*70) - - -def verify_solver(): - """ - Comprehensive verification of Hand-wave solver. - - Tests three fundamental potentials against analytical solutions: - 1. Infinite Square Well (Particle in a Box) - 2. Finite Square Well - 3. Harmonic Oscillator - - Prints all results directly to notebook (no files created). - - Usage in notebook - ----------------- - >>> from functions import verify_solver - >>> verify_solver() - """ - print("\n" + "="*80) - print(" "*20 + "HAND-WAVE SOLVER VERIFICATION") - print("="*80) - print("\nTesting against analytical solutions for fundamental quantum systems") - print("-"*80) - - # ======================================== - # TEST 1: Infinite Square Well - # ======================================== - print("\n" + "="*80) - print("[TEST 1] INFINITE SQUARE WELL (Particle in a Box)") - print("="*80) - - L_isw = 20.0 - N_isw = 1000 - print(f"Domain: L = {L_isw} a.u., Grid points: N = {N_isw}") - - x_isw, dx_isw, x_int_isw = make_grid(L=L_isw, N=N_isw) - - V_isw = np.zeros_like(x_isw) - V_isw[0] = 1e10 - V_isw[-1] = 1e10 - - T_isw = kinetic_operator(N_isw, dx_isw) - E_isw, psi_isw = solve(T_isw, V_isw, dx_isw) - - print(f"\n✓ Solved for {len(E_isw)} eigenstates") - print(f" Ground state energy: E[0] = {E_isw[0]:.6f} Ha") - - # Compare with analytical - E_anal_isw, E_num_isw = check_ISW_analytic(E_isw, lower_bound=-L_isw/2, upper_bound=L_isw/2, max_levels=5) - - # ======================================== - # TEST 2: Finite Square Well - # ======================================== - print("\n" + "="*80) - print("[TEST 2] FINITE SQUARE WELL") - print("="*80) - - L_fsw = 20.0 - N_fsw = 1000 - V0_fsw = 2.0 # Deep well for bound states - - print(f"Domain: L = {L_fsw} a.u., Grid points: N = {N_fsw}") - print(f"Barrier height: V₀ = {V0_fsw} Ha") - - x_fsw, dx_fsw, x_int_fsw = make_grid(L=L_fsw, N=N_fsw) - - V_int_fsw = finite_square_well(x_int_fsw, lower_bound=-10, upper_bound=10, depth_V=V0_fsw) - V_fsw = np.zeros_like(x_fsw) - V_fsw[1:-1] = V_int_fsw - V_fsw[0] = 1e10 - V_fsw[-1] = 1e10 - - T_fsw = kinetic_operator(N_fsw, dx_fsw) - E_fsw, psi_fsw = solve(T_fsw, V_fsw, dx_fsw) - - # Count bound states - n_bound = np.sum(E_fsw < V0_fsw) - print(f"\n✓ Solved for {len(E_fsw)} eigenstates") - print(f" Bound states (E < V₀): {n_bound}") - print(f" Ground state energy: E[0] = {E_fsw[0]:.6f} Ha") - - # Compare with analytical - E_anal_fsw, E_num_fsw = check_finite_well_analytic(E_fsw, V0=V0_fsw, lower_bound=-10, upper_bound=10, max_levels=10) - - # ======================================== - # TEST 3: Harmonic Oscillator - # ======================================== - print("\n" + "="*80) - print("[TEST 3] HARMONIC OSCILLATOR") - print("="*80) - - L_ho = 50.0 - N_ho = 2000 - k_ho = 1.0 - - print(f"Domain: L = {L_ho} a.u., Grid points: N = {N_ho}") - print(f"Spring constant: k = {k_ho}") - - x_ho, dx_ho, x_int_ho = make_grid(L=L_ho, N=N_ho) - - V_int_ho = harmonic(x_int_ho, k=k_ho, center=0.0) - V_ho = np.zeros_like(x_ho) - V_ho[1:-1] = V_int_ho - V_ho[0] = 1e10 - V_ho[-1] = 1e10 - - T_ho = kinetic_operator(N_ho, dx_ho) - E_ho, psi_ho = solve(T_ho, V_ho, dx_ho) - - print(f"\n✓ Solved for {len(E_ho)} eigenstates") - print(f" Ground state energy: E[0] = {E_ho[0]:.6f} Ha") - print(f" Expected (analytical): E[0] = 0.500000 Ha") - - # Compare with analytical - E_anal_ho, E_num_ho = check_harmonic_analytic(E_ho, k=k_ho, max_levels=5) - - # ======================================== - # SUMMARY - # ======================================== - print("\n" + "="*80) - print("VERIFICATION SUMMARY") - print("="*80) - - # Calculate average errors - err_isw = np.mean(np.abs((E_num_isw - E_anal_isw) / E_anal_isw) * 100) - err_ho = np.mean(np.abs((E_num_ho - E_anal_ho) / E_anal_ho) * 100) - - print(f"\n{'Test':<30} {'Avg Error':<15} {'Status':<15}") - print("-"*60) - print(f"{'Infinite Square Well':<30} {err_isw:<14.4f}% {'✅ PASS' if err_isw < 0.01 else '⚠️ CHECK':<15}") - print(f"{'Harmonic Oscillator':<30} {err_ho:<14.4f}% {'✅ PASS' if err_ho < 0.02 else '⚠️ CHECK':<15}") - - if E_anal_fsw is not None: - err_fsw = np.mean(np.abs((E_num_fsw - E_anal_fsw) / E_anal_fsw) * 100) - print(f"{'Finite Square Well':<30} {err_fsw:<14.4f}% {'✅ PASS' if err_fsw < 0.5 else '⚠️ CHECK':<15}") - else: - print(f"{'Finite Square Well':<30} {'N/A':<14} {'⚠️ No bound states':<15}") - - print("-"*60) - - # Overall verdict - print("\n" + "="*80) - if err_isw < 0.01 and err_ho < 0.02: - print("✅ VERIFICATION PASSED: Solver is accurate and validated!") - else: - print("⚠️ VERIFICATION WARNING: Check solver implementation") - print("="*80) - print() - - - -# ========================================== -# VERIFICATION FUNCTION FOR NOTEBOOKS -# ========================================== - -def run_verification(): - """ - Comprehensive physics verification tests. - - Tests multiple potentials against analytical solutions: - 1. Infinite Square Well - 2. Harmonic Oscillator - 3. Half-Harmonic Oscillator - 4. Triangular Potential - 5. Hamiltonian Construction Verification - - Results are saved to 'verification_log.txt' - - Usage - ----- - >>> from functions import run_verification - >>> run_verification() - """ - import sys - - with open("verification_log.txt", "w") as log_file: - sys.stdout = log_file - print("========================================") - print("PHYSICS ENGINE VERIFICATION") - print("========================================") - - # 1. Infinite Square Well Test - print("\n[TEST 1] Infinite Square Well (Particle in a Box)") - L = 20.0 - N = 1000 - x_full, dx, x_internal = make_grid(L=L, N=N) - - V_full = np.zeros_like(x_full) - V_full[0] = 1e10 - V_full[-1] = 1e10 - - T = kinetic_operator(N, dx) - E, psi = solve(T, V_full, dx) - - check_ISW_analytic(E, lower_bound=-L/2, upper_bound=L/2, max_levels=5) - check_ortho(psi, dx, num_states_to_check=5) - - # 2. Harmonic Oscillator Test - print("\n[TEST 2] Harmonic Oscillator") - L_HO = 50.0 - N_HO = 2000 - x_full, dx, x_internal = make_grid(L=L_HO, N=N_HO) - - k = 1.0 - V_internal = harmonic(x_internal, k=k) - - V_full = np.zeros_like(x_full) - V_full[1:-1] = V_internal - V_full[0] = 1e10 - V_full[-1] = 1e10 - - T = kinetic_operator(N_HO, dx) - E, psi = solve(T, V_full, dx) - - check_harmonic_analytic(E, k=k, max_levels=5) - - # 3. Half-Harmonic Oscillator Test - print("\n[TEST 3] Half-Harmonic Oscillator") - L_HH = 20.0 - N_HH = 1000 - x_full, dx, x_internal = make_grid(L=L_HH, N=N_HH) - - k = 1.0 - V_internal = 0.5 * k * x_internal**2 - V_internal[x_internal <= 0] = 1e10 - - V_full = np.zeros_like(x_full) - V_full[1:-1] = V_internal - V_full[0] = 1e10 - V_full[-1] = 1e10 - - T = kinetic_operator(N_HH, dx) - E, psi = solve(T, V_full, dx) - - w = np.sqrt(k/1.0) - print("\n### ENERGY BENCHMARK: Half-Harmonic Oscillator ###") - print("-" * 55) - print(f"| n | Analytic E | Numerical E | % Error |") - print("-" * 55) - for i in range(5): - E_analytic = (2*i + 1.5) * 1.0 * w - percent_error = np.abs((E[i] - E_analytic) / E_analytic) * 100 - print(f"| {i:<1} | {E_analytic:<10.6f} | {E[i]:<11.6f} | {percent_error:<7.4f}% |") - print("-" * 55) - - # 4. Triangular Potential Test - print("\n[TEST 4] Triangular Potential V(x) = alpha * |x|") - L_Tri = 30.0 - N_Tri = 2000 - x_full, dx, x_internal = make_grid(L=L_Tri, N=N_Tri) - - alpha = 1.0 - V_internal = alpha * np.abs(x_internal) - - V_full = np.zeros_like(x_full) - V_full[1:-1] = V_internal - V_full[0] = 1e10 - V_full[-1] = 1e10 - - T = kinetic_operator(N_Tri, dx) - E, psi = solve(T, V_full, dx) - - zeros = [1.01879, 2.33811, 3.24820, 4.08795, 4.82010] - prefactor = (1**2 * alpha**2 / (2*1))**(1/3) - - print("\n### ENERGY BENCHMARK: Triangular Potential ###") - print("-" * 55) - print(f"| n | Analytic E | Numerical E | % Error |") - print("-" * 55) - for i in range(5): - E_analytic = prefactor * zeros[i] - percent_error = np.abs((E[i] - E_analytic) / E_analytic) * 100 - print(f"| {i:<1} | {E_analytic:<10.6f} | {E[i]:<11.6f} | {percent_error:<7.4f}% |") - print("-" * 55) - - # 5. Code Verification - print("\n[TEST 5] Hamiltonian Construction Verification") - print("Checking kinetic_operator...") - print("Confirmed: 3-point central difference stencil (1, -2, 1) used for Laplacian.") - print("Confirmed: Pre-factor -hbar^2/(2m) applied correctly.") - - sys.stdout = sys.__stdout__ - print("\n✓ Verification complete! Results saved to 'verification_log.txt'") - - -## - - - - - -def display_params(frame, params_list, start_y=80, line_height=25, color=(255, 255, 255)): - for i, text in enumerate(params_list): - y = start_y + i * line_height - cv2.putText(frame, text, (10, y), cv2.FONT_HERSHEY_SIMPLEX, - 0.6, (0, 0, 0), 3) - cv2.putText(frame, text, (10, y), cv2.FONT_HERSHEY_SIMPLEX, - 0.6, color, 2) - - -# --------------------------------------------------------------------- -# MAIN FUNCTION: HAND-CONTROLLED POTENTIAL CAPTURE -# --------------------------------------------------------------------- - -# --------------------------------------------------------------------- -# INITIALIZATION -# --------------------------------------------------------------------- -mp_hands = mp.solutions.hands -hands = mp_hands.Hands(max_num_hands=2, min_detection_confidence=0.7) -drawer = mp.solutions.drawing_utils - - -def capture_potential(tune, A_MIN, A_MAX, mode='wait'): - - cap = cv2.VideoCapture(0) - captured_V = None - - # Stability tracking ----------------------------------------------- - stability_counter = 0 - REQUIRED_STABLE_FRAMES = 45 - MOVEMENT_THRESHOLD = 0.015 - prev_landmarks = [] - - # Landmark indices -------------------------------------------------- - THUMB_TIP_ID = 4 - INDEX_TIP_ID = 8 - - # QHO Mapping constants -------------------------------------------- - D_MIN = 0.001 - D_MAX = 0.2 - - D_RANGE = D_MAX - D_MIN - A_RANGE = A_MAX - A_MIN - - SLOPE = -A_RANGE / D_RANGE - INTERCEPT = A_MAX - SLOPE * D_MIN - - # Fixed visual scale (independent of physics range) - PLOT_CEILING_A = 10.0 - EPS = 1e-9 - - print("Controls: HOLD STILL to capture, or press 'q' to quit.") - - # ================================================================= - # MAIN LOOP - # ================================================================= - while True: - ret, frame = cap.read() - if not ret: - break - - frame = cv2.flip(frame, 1) - h, w, _ = frame.shape - - rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - res = hands.process(rgb) - - pot_profile = None - mode_msg = "No Hands" - params_to_display = [] - current_landmarks_flat = [] - - # -------------------------------------------------------------- - # LANDMARK PROCESSING - # -------------------------------------------------------------- - if res.multi_hand_landmarks: - - # Flatten positions for stability detection - for hand_lms in res.multi_hand_landmarks: - for lm in hand_lms.landmark: - current_landmarks_flat.extend([lm.x, lm.y]) - - # Draw detected hands - for lm in res.multi_hand_landmarks: - drawer.draw_landmarks(frame, lm, mp_hands.HAND_CONNECTIONS) - - # ---------------------------------------------------------- - # TWO HANDS = SQUARE WELL (AUTO-CENTERED) - # ---------------------------------------------------------- - if len(res.multi_hand_landmarks) >= 2: - mode_msg = "Mode: Square Well (Auto-Centered)" - - # 1. Get Hand Positions - x_coords = [ - lm.landmark[INDEX_TIP_ID].x * w - for lm in res.multi_hand_landmarks - ] - x_coords.sort() - xL_hand, xR_hand = int(x_coords[0]), int(x_coords[1]) - - # 2. Draw Yellow lines at REAL hand positions (Visual Feedback) - cv2.line(frame, (xL_hand, 0), (xL_hand, h), (0, 255, 255), 2) - cv2.line(frame, (xR_hand, 0), (xR_hand, h), (0, 255, 255), 2) - - # 3. Calculate Force-Centered Coordinates - # We calculate the width of your hands, but ignore their position - well_width = xR_hand - xL_hand - center_screen = w / 2 - - # Create boundaries centered on the screen - centered_L = center_screen - (well_width / 2) - centered_R = center_screen + (well_width / 2) - - params_to_display.append(f"Width: {well_width:4.0f} px") - params_to_display.append(f"Status: Centered") - - # 4. Generate Potential (Centered) - x_space = np.linspace(0, w, 400) - pot_profile = np.ones_like(x_space) - # Use centered_L/R instead of hand positions - pot_profile[(x_space > centered_L) & (x_space < centered_R)] = 0 - - """ - # 5. Visualize the Centered Potential (Red Line) - display_pts = np.column_stack(( - x_space, - pot_profile * (h - 10) # simple scaling for viz - )).astype(np.int32) - cv2.polylines(frame, [display_pts], False, (0, 0, 255), 2) - """ - # ---------------------------------------------------------- - # ONE HAND = PINCH PARABOLA (QHO) - # ---------------------------------------------------------- - elif len(res.multi_hand_landmarks) == 1: - mode_msg = "Mode: Pinch QHO" - lm = res.multi_hand_landmarks[0] - - thumb = lm.landmark[THUMB_TIP_ID] - index = lm.landmark[INDEX_TIP_ID] - - dx = index.x - thumb.x - dy = index.y - thumb.y - pinch_distance = math.sqrt(dx**2 + dy**2) - - # Compute curvature - A = SLOPE * pinch_distance + INTERCEPT - A = max(A_MIN, min(A_MAX, A)) - - # This is already mathematically centered at 0 - x_space = np.linspace(-1, 1, 400) - pot_profile = A * (x_space**2) - - # Fixed visual scale - pot_profile = pot_profile / (PLOT_CEILING_A + EPS) - pot_profile = np.clip(pot_profile, 0.0, 1.0) - - params_to_display.append(f"Pinch Dist: {pinch_distance:.4f}") - params_to_display.append(f"A (curv): {A:.4f}") - - display_pts = np.column_stack(( - (x_space + 1)/2 * w, - (1 - pot_profile) * h - )).astype(np.int32) - - cv2.polylines(frame, [display_pts], False, (0, 0, 255), 2) - - # ============================================================== - # STABILITY CHECK - # ============================================================== - if mode != 'wait': - if current_landmarks_flat and prev_landmarks: - if len(current_landmarks_flat) == len(prev_landmarks): - movement = np.mean(np.abs( - np.array(current_landmarks_flat) - - np.array(prev_landmarks) - )) - if movement < MOVEMENT_THRESHOLD: - stability_counter += 1 - else: - stability_counter = 0 - else: - stability_counter = 0 - else: - stability_counter = 0 - - prev_landmarks = current_landmarks_flat - - # Show loading bar - if stability_counter > 0: - progress = stability_counter / REQUIRED_STABLE_FRAMES - bar_width = int(w * progress) - color = (0, 255*progress, 255*(1-progress)) - cv2.rectangle(frame, (0, 0), (bar_width, 20), color, -1) - cv2.putText(frame, "HOLDING...", (10, 15), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) - - # Finished - if stability_counter >= REQUIRED_STABLE_FRAMES and pot_profile is not None: - captured_V = pot_profile - frame[:] = 255 - cv2.imshow("Quantum Potential Input", frame) - cv2.waitKey(100) - print("Stable capture triggered!") - break - - # -------------------------------------------------------------- - # UI OVERLAY - # -------------------------------------------------------------- - cv2.putText(frame, mode_msg, (10, 50), - cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) - - display_params(frame, params_to_display) - cv2.imshow("Quantum Potential Input", frame) - - if cv2.waitKey(1) & 0xFF == ord('q'): - break - - # ----------------------------------------------------------------- - cap.release() - cv2.destroyAllWindows() - return captured_V - -# Create a notebook-friendly version of the function -def cheese(tune, A_MIN, A_MAX, mode='wait'): - import time - from IPython.display import display, Image, clear_output - - - # Copy relevant constants from the file for local scope - THUMB_TIP_ID = 4 - INDEX_TIP_ID = 8 - REQUIRED_STABLE_FRAMES = 45 - MOVEMENT_THRESHOLD = 0.015 - PLOT_CEILING_A = 10.0 - EPS = 1e-9 - - D_MIN = 0.001 - D_MAX = 0.2 - D_RANGE = D_MAX - D_MIN - A_RANGE = A_MAX - A_MIN - SLOPE = -A_RANGE / D_RANGE - INTERCEPT = A_MAX - SLOPE * D_MIN - # End of copied constants - - cap = cv2.VideoCapture(0) - captured_V = None - - if not cap.isOpened(): - print("Error: Could not open video stream. Check permissions or camera index.") - return None - - stability_counter = 0 - prev_landmarks = [] - - start_time = time.time() - MAX_RUN_TIME_SECONDS = 30 - - print("Controls: HOLD STILL to capture, or wait for the time limit to exit.") - - while True: - ret, frame = cap.read() - if not ret: - break - - frame = cv2.flip(frame, 1) - h, w, _ = frame.shape - rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - res = hands.process(rgb) - - pot_profile = None - mode_msg = "No Hands" - params_to_display = [] - current_landmarks_flat = [] - - # --- LANDMARK AND POTENTIAL LOGIC (Skipped for brevity, assume this is correct) --- - if res.multi_hand_landmarks: - for hand_lms in res.multi_hand_landmarks: - for lm in hand_lms.landmark: - current_landmarks_flat.extend([lm.x, lm.y]) - for lm in res.multi_hand_landmarks: - drawer.draw_landmarks(frame, lm, mp_hands.HAND_CONNECTIONS) - - # TWO HANDS (Square Well) - if len(res.multi_hand_landmarks) >= 2: - mode_msg = "Mode: Square Well (Auto-Centered)" - x_coords = [lm.landmark[INDEX_TIP_ID].x * w for lm in res.multi_hand_landmarks] - x_coords.sort() - xL_hand, xR_hand = int(x_coords[0]), int(x_coords[1]) - cv2.line(frame, (xL_hand, 0), (xL_hand, h), (0, 255, 255), 2) - cv2.line(frame, (xR_hand, 0), (xR_hand, h), (0, 255, 255), 2) - well_width = xR_hand - xL_hand - center_screen = w / 2 - centered_L = center_screen - (well_width / 2) - centered_R = center_screen + (well_width / 2) - params_to_display.append(f"Width: {well_width:4.0f} px") - params_to_display.append(f"Status: Centered") - x_space = np.linspace(0, w, 400) - pot_profile = np.ones_like(x_space) - pot_profile[(x_space > centered_L) & (x_space < centered_R)] = 0 - # ONE HAND (QHO) - elif len(res.multi_hand_landmarks) == 1: - mode_msg = "Mode: Pinch QHO" - lm = res.multi_hand_landmarks[0] - thumb = lm.landmark[THUMB_TIP_ID] - index = lm.landmark[INDEX_TIP_ID] - dx = index.x - thumb.x - dy = index.y - thumb.y - pinch_distance = math.sqrt(dx**2 + dy**2) - A = SLOPE * pinch_distance + INTERCEPT - A = max(A_MIN, min(A_MAX, A)) - x_space = np.linspace(-1, 1, 400) - pot_profile = A * (x_space**2) - pot_profile = pot_profile / (PLOT_CEILING_A + EPS) - pot_profile = np.clip(pot_profile, 0.0, 1.0) - params_to_display.append(f"Pinch Dist: {pinch_distance:.4f}") - params_to_display.append(f"A (curv): {A:.4f}") - display_pts = np.column_stack(((x_space + 1)/2 * w, (1 - pot_profile) * h)).astype(np.int32) - cv2.polylines(frame, [display_pts], False, (0, 0, 255), 2) - # --- END LANDMARK AND POTENTIAL LOGIC --- - - # STABILITY CHECK - if mode != 'wait': - if current_landmarks_flat and prev_landmarks and len(current_landmarks_flat) == len(prev_landmarks): - movement = np.mean(np.abs(np.array(current_landmarks_flat) - np.array(prev_landmarks))) - stability_counter = stability_counter + 1 if movement < MOVEMENT_THRESHOLD else 0 - else: - stability_counter = 0 - - prev_landmarks = current_landmarks_flat - - if stability_counter > 0: - progress = stability_counter / REQUIRED_STABLE_FRAMES - bar_width = int(w * progress) - color = (0, 255*progress, 255*(1-progress)) - cv2.rectangle(frame, (0, 0), (bar_width, 20), color, -1) - cv2.putText(frame, "HOLDING...", (10, 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) - - # Finished - if stability_counter >= REQUIRED_STABLE_FRAMES and pot_profile is not None: - captured_V = pot_profile - cap.release() - # --- LINE REMOVED HERE (was cv2.destroyAllWindows()) --- - print("Stable capture triggered and video stream closed.") - return captured_V - - # UI OVERLAY - cv2.putText(frame, mode_msg, (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) - display_params(frame, params_to_display) - - # NOTEBOOK DISPLAY - clear_output(wait=True) - _, buffer = cv2.imencode('.jpeg', frame) - display(Image(data=buffer.tobytes())) - - time.sleep(0.01) - - if time.time() - start_time > MAX_RUN_TIME_SECONDS: - print(f"Time limit of {MAX_RUN_TIME_SECONDS} seconds reached.") - break - - # ----------------------------------------------------------------- - cap.release() - return captured_V - - - - - -### -import qrcode -from IPython.display import display, Image - -def show_QR(url): - # The file name to save the QR code image - file_name = "hand_wave_link_qrcode.png" - - # --- QR Code Generation --- - # 1. Create a QR code object with specific settings - qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=10, - border=4, - ) - - # 2. Add the URL data to the object - qr.add_data(url) - qr.make(fit=True) - - # 3. Create the QR code image - img = qr.make_image(fill_color="black", back_color="white") - - # 4. Save the image to the local directory - img.save(file_name) - - # --- Display in Jupyter Notebook --- - - # 5. Display the saved image using IPython.display - return display(Image(filename=file_name)) - - - - - +# psi_solve2/functions.py + +import numpy as np +import matplotlib.pyplot as plt + +import math +from matplotlib.ticker import MultipleLocator + +# ========================================== +# 1. PHYSICS CONSTANTS +# ========================================== +hbar = 1 +m = 1 +L = 50 +N_GRID = 2000 +global Last_k_value # Used by harmonic() and check_harmonic_analytic() + +# ========================================== +# 2. GRID FUNCTIONS +# ========================================== +def make_grid(L=L, N=N_GRID): + """ + Create a spatial grid for solving the Schrödinger equation. + + Parameters + ---------- + L : float, optional + Total length of the spatial domain (default: 50 a.u.) + N : int, optional + Number of internal grid points (default: 2000) + + Returns + ------- + x_full : ndarray + Full grid with N+2 points from -L/2 to L/2, including boundary points + dx : float + Grid spacing (distance between adjacent points) + x_internal : ndarray + Internal grid points (N points) where the wavefunction is solved + Excludes the boundary points at x[0] and x[-1] + + Notes + ----- + The boundary points are used to enforce boundary conditions (typically ψ=0) + while x_internal contains the points where we actually solve for ψ. + + Examples + -------- + >>> x, dx, x_int = make_grid(L=20, N=1000) + >>> print(f"Domain: [{x[0]:.1f}, {x[-1]:.1f}], spacing: {dx:.4f}") + Domain: [-10.0, 10.0], spacing: 0.0200 + """ + x = np.linspace(-L/2, L/2, N+2) + dx = x[1] - x[0] + x_internal = x[1:-1] + return x, dx, x_internal + +# ========================================== +# 3. POTENTIAL GENERATORS (V(x)) +# ========================================== +def constant(x, c): + """ + Create a constant potential across the entire domain. + + Parameters + ---------- + x : ndarray + Spatial grid points + c : float + Constant potential value (in Hartree atomic units) + + Returns + ------- + V : ndarray + Constant potential array of same shape as x, with value c everywhere + + Examples + -------- + >>> x = np.linspace(-10, 10, 100) + >>> V = constant(x, 5.0) # V(x) = 5.0 everywhere + """ + return np.ones_like(x) * c + +def harmonic(x, k, center=0.0): + """ + Create a harmonic oscillator (parabolic) potential. + + Generates V(x) = (1/2)k(x - center)² representing a quantum harmonic + oscillator potential centered at the specified position. + + Parameters + ---------- + x : ndarray + Spatial grid points + k : float + Spring constant (curvature parameter) in atomic units + Larger k → stiffer spring → more tightly bound states + center : float, optional + Center position of the parabola (default: 0.0) + + Returns + ------- + V : ndarray + Harmonic potential array: V(x) = 0.5 * k * (x - center)² + + Notes + ----- + - Sets global variable Last_k_value for use by check_harmonic_analytic() + - Energy levels: E_n = ℏω(n + 1/2) where ω = √(k/m) + - In atomic units (ℏ=1, m=1): ω = √k + + Examples + -------- + >>> x = np.linspace(-10, 10, 1000) + >>> V = harmonic(x, k=1.0, center=0.0) # Standard QHO + >>> V_stiff = harmonic(x, k=10.0, center=0.0) # Stiffer spring + >>> V_offset = harmonic(x, k=1.0, center=5.0) # Centered at x=5 + """ + global Last_k_value + Last_k_value = k + + constant_factor = 1 + potential = 0.5 * k * (x - center)**2 + return constant_factor * potential + +def gaussian_well(x, center=0.0, width=1.0, depth=50): + """ + Create a Gaussian-shaped potential well. + + Generates a smooth, bell-shaped potential dip that can trap particles. + + Parameters + ---------- + x : ndarray + Spatial grid points + center : float, optional + Center position of the well (default: 0.0) + width : float, optional + Width parameter (standard deviation) of the Gaussian (default: 1.0) + Larger width → broader well + depth : float, optional + Depth of the well at the center (default: 50) + Positive depth creates a well (attractive potential) + + Returns + ------- + V : ndarray + Gaussian well potential: V(x) = -depth * exp(-(x-center)²/(2*width²)) + + Notes + ----- + - Minimum potential is -depth at x = center + - Potential approaches 0 as |x - center| → ∞ + - Smooth potential (infinitely differentiable) + + Examples + -------- + >>> x = np.linspace(-10, 10, 1000) + >>> V = gaussian_well(x, center=0, width=2.0, depth=10) + """ + return -depth * np.exp(-(x - center)**2 / (2 * width**2)) + +def inf_sqaure_well(x, lower_bound, upper_bound): + """ + Create an infinite square well (particle in a box) potential. + + Parameters + ---------- + x : ndarray + Spatial grid points + lower_bound : float + Left boundary of the well + upper_bound : float + Right boundary of the well + + Returns + ------- + V : ndarray + Infinite square well potential: + - V(x) = 0 for lower_bound ≤ x ≤ upper_bound (inside well) + - V(x) = 10¹⁰ for x < lower_bound or x > upper_bound (outside well) + + Notes + ----- + - Uses penalty method: "infinite" walls are approximated by very large + potential (10¹⁰) to enforce ψ ≈ 0 outside the well + - Well width: L = upper_bound - lower_bound + - Analytical energies: E_n = (ℏ²π²n²)/(2mL²) for n = 1, 2, 3, ... + + Examples + -------- + >>> x = np.linspace(-15, 15, 1000) + >>> V = inf_sqaure_well(x, lower_bound=-10, upper_bound=10) # L = 20 + >>> # Use with check_ISW_analytic(E, lower_bound=-10, upper_bound=10) + """ + HUGE_NUMBER = 1e10 + V = np.zeros_like(x) + V[x < lower_bound] = HUGE_NUMBER + V[x > upper_bound] = HUGE_NUMBER + return V + +def inf_wall(x, side, bound): + """ + Place an infinite potential wall on one side of the domain. + + Parameters + ---------- + x : ndarray + Spatial grid points + side : str + Which side to place the wall: 'left' or 'right' + (case-insensitive, strips whitespace and punctuation) + bound : float + Position of the wall boundary + + Returns + ------- + V : ndarray + Potential with infinite wall: + - If side='left': V(x) = 10¹⁰ for x < bound, V(x) = 0 for x ≥ bound + - If side='right': V(x) = 10¹⁰ for x > bound, V(x) = 0 for x ≤ bound + + Notes + ----- + Uses penalty method with V = 9×10¹⁰ to approximate infinite potential. + + Examples + -------- + >>> x = np.linspace(-10, 10, 1000) + >>> V_left = inf_wall(x, 'left', bound=-5) # Wall at x=-5, blocks left side + >>> V_right = inf_wall(x, 'right', bound=5) # Wall at x=5, blocks right side + """ + V = np.zeros_like(x) + HUGE_NUMBER = 9e10 + side = side.strip(', . ').lower() + + if side == 'left': + V[x < bound] = HUGE_NUMBER + elif side == 'right': + V[x > bound] = HUGE_NUMBER + return V + +def finite_barrier(x, center, width, height): + """ + Create a finite rectangular potential barrier. + + Parameters + ---------- + x : ndarray + Spatial grid points + center : float + Center position of the barrier + width : float + Total width of the barrier + height : float + Height of the potential barrier + + Returns + ------- + V : ndarray + Rectangular barrier potential: + - V(x) = height for |x - center| < width/2 + - V(x) = 0 elsewhere + + Notes + ----- + Useful for studying quantum tunneling phenomena. Particles with E < height + can tunnel through the barrier with exponentially decaying probability. + + Examples + -------- + >>> x = np.linspace(-10, 10, 1000) + >>> V = finite_barrier(x, center=0, width=2, height=5) # Barrier from x=-1 to x=1 + """ + V = np.zeros_like(x) + mask = (x > (center - width/2)) & (x < (center + width/2)) + V[mask] = height + return V + +def V_double_well(x, depth=20, separation=1, center=0.0): + """ + Create a quartic double-well potential. + + Generates V(x) = depth × ((x-center)² - separation)² which has two minima + separated by a central barrier. + + Parameters + ---------- + x : ndarray + Spatial grid points + depth : float, optional + Depth parameter controlling overall potential strength (default: 20) + separation : float, optional + Controls the distance between the two wells (default: 1) + Well minima are approximately at x = center ± separation + center : float, optional + Center position of the double well system (default: 0.0) + + Returns + ------- + V : ndarray + Double well potential: V(x) = depth × ((x-center)² - separation)² + + Notes + ----- + - Creates symmetric double well with barrier at x = center + - Useful for studying tunneling splitting and symmetric/antisymmetric states + - Ground state and first excited state form tunneling doublet + + Examples + -------- + >>> x = np.linspace(-5, 5, 1000) + >>> V = V_double_well(x, depth=2, separation=1, center=0) + """ + V = depth * ((x - center)**2 - separation)**2 + return V + +def custom2(value,x): + """Helper function from the notebook.""" + return value * np.ones_like(x) + +# In psi_solve2/functions.py + +def finite_square_well(x, lower_bound, upper_bound, depth_V): + """ + Create a finite square well potential. + + The potential is zero inside the well and has finite height depth_V outside. + Unlike the infinite square well, particles can exist in the barrier region + with exponentially decaying wavefunctions. + + Parameters + ---------- + x : ndarray + Spatial grid points + lower_bound : float + Left boundary of the well + upper_bound : float + Right boundary of the well + depth_V : float + Height of the potential barriers outside the well (V₀) + + Returns + ------- + V : ndarray + Finite square well potential: + - V(x) = 0 for lower_bound ≤ x ≤ upper_bound (inside well) + - V(x) = depth_V for x < lower_bound or x > upper_bound (barrier regions) + + """ + """ + Notes + ----- + - Bound states exist only when E < depth_V + - Number of bound states depends on well width and depth_V + - For bound states, wavefunction decays exponentially in barrier (E < V) + - For scattering states (E > depth_V), wavefunction oscillates everywhere + - Use check_finite_well_analytic() to verify numerical results + + Examples + -------- + >>> x = np.linspace(-15, 15, 1000) + >>> V_deep = finite_square_well(x, -10, 10, depth_V=2.0) # Deep well, many bound states + >>> V_shallow = finite_square_well(x, -10, 10, depth_V=0.01) # Shallow, few/no bound states + """ + # Start with a baseline of zero potential + V = np.zeros_like(x) + + # The walls outside the well are set to the height/depth V_0 + V[x < lower_bound] = depth_V + V[x > upper_bound] = depth_V + + # The potential *inside* the well remains V=0 (or whatever you set the baseline to) + return V + +# ========================================== +# 4. SCHRÖDINGER EQUATION SOLVER +# ========================================== +def kinetic_operator(N, dx, hbar=hbar, m=m): + """ + Build the kinetic energy operator matrix using finite difference method. + + Constructs the discrete representation of the kinetic energy operator + T = -(ℏ²/2m) d²/dx² using a 3-point central difference stencil. + + Parameters + ---------- + N : int + Number of internal grid points (size of the matrix) + dx : float + Grid spacing (distance between adjacent points) + hbar : float, optional + Reduced Planck constant (default: 1.0 in atomic units) + m : float, optional + Particle mass (default: 1.0 in atomic units) + + Returns + ------- + T : ndarray, shape (N, N) + Kinetic energy operator matrix (symmetric, tridiagonal) + - Diagonal elements: -(ℏ²/2m) × (-2/dx²) + - Off-diagonal elements: -(ℏ²/2m) × (1/dx²) + + """ + """ + Notes + ----- + The second derivative is approximated using central differences: + d²ψ/dx² ≈ (ψ_{i+1} - 2ψ_i + ψ_{i-1}) / dx² + + This creates a tridiagonal matrix: + - Main diagonal: -2/dx² + - Upper/lower diagonals: +1/dx² + + The kinetic energy operator is then: T = -(ℏ²/2m) × D2 + + Examples + -------- + >>> N = 1000 + >>> dx = 0.025 + >>> T = kinetic_operator(N, dx) + >>> print(f"Matrix shape: {T.shape}, Symmetric: {np.allclose(T, T.T)}") + Matrix shape: (1000, 1000), Symmetric: True + """ + main_diagonal = (1/dx**2) * np.diag(-2 * np.ones(N)) + off_diagonal1 = (1/dx**2) * np.diag(np.ones(N-1), -1) + off_diagonal2 = (1/dx**2) * np.diag(np.ones(N-1), 1) + D2 = (main_diagonal + off_diagonal1 + off_diagonal2) + + T = (-(hbar**2 / (2*m)) * D2) + return T + +def solve(T, V_full, dx): + """ + Solve the time-independent Schrödinger equation for eigenvalues and eigenvectors. + + Solves the eigenvalue problem Hψ = Eψ where H = T + V is the Hamiltonian. + Returns normalized eigenstates sorted by energy. + + Parameters + ---------- + T : ndarray, shape (N, N) + Kinetic energy operator matrix from kinetic_operator() + V_full : ndarray, shape (N+2,) + Full potential array including boundary points + V_full[0] and V_full[-1] are boundary values (typically very large) + V_full[1:-1] are the internal potential values + dx : float + Grid spacing used for normalization + + Returns + ------- + E : ndarray, shape (N,) + Eigenvalues (energy levels) sorted in ascending order + Units: Hartree (atomic units) + psi : ndarray, shape (N, N) + Eigenvectors (wavefunctions) as columns + psi[:, i] is the wavefunction for energy E[i] + Each wavefunction is normalized: ∫|ψ|² dx = 1 + + """ + """ + Notes + ----- + - Uses np.linalg.eigh() which assumes Hermitian matrix (guaranteed for H) + - Automatically sorts eigenvalues and eigenvectors by energy + - Normalizes each eigenstate using trapezoidal rule: ∫|ψ|² dx = 1 + - Boundary conditions are enforced by V_full having large values at edges + + The Hamiltonian is constructed as: + H = T + diag(V_internal) + where V_internal = V_full[1:-1] + + Examples + -------- + >>> # Setup + >>> x, dx, x_int = make_grid(L=20, N=1000) + >>> T = kinetic_operator(len(x_int), dx) + >>> + >>> # Create infinite square well + >>> V = inf_sqaure_well(x_int, -10, 10) + >>> V_full = np.pad(V, (1,1), constant_values=1e10) + >>> + >>> # Solve + >>> E, psi = solve(T, V_full, dx) + >>> print(f"Ground state energy: {E[0]:.6f} Ha") + >>> + >>> # Verify normalization + >>> norm = np.sum(psi[:, 0]**2) * dx + >>> print(f"Normalization: {norm:.6f}") # Should be 1.0 + """ + V_internal = V_full[1:-1] + H = T + np.diag(V_internal) + + E, psi = np.linalg.eigh(H) + + # Normalize each state individually + for i in range(psi.shape[1]): + # sum( |psi|^2 * dx ) + norm_factor = np.sum(psi[:, i]**2) * dx + # Divide by the square root of the integral + psi[:, i] = psi[:, i] / np.sqrt(norm_factor) + + return E, psi + +# ========================================== +# 5. PLOTTING FUNCTIONS (STREAMLIT/JUPYTER SAFE) +# ========================================== +def plot_V(V_raw_input): + """ + Plot a 1D potential profile. + + Creates a simple matplotlib figure showing the potential energy landscape. + + Parameters + ---------- + V_raw_input : ndarray or None + 1D array representing the potential V(x) + If None or scalar, returns None + + Returns + ------- + fig : matplotlib.figure.Figure or None + Figure object containing the potential plot + Returns None if input is invalid + """ + """ + Notes + ----- + - Uses dark background style + - Cyan color for potential curve + - Useful for quick visualization of potential shapes + + Examples + -------- + >>> x = np.linspace(-10, 10, 1000) + >>> V = harmonic(x, k=1.0) + >>> fig = plot_V(V) + >>> plt.show() + """ + if V_raw_input is None or np.ndim(V_raw_input) == 0: + return None + + plt.style.use("dark_background") + fig, ax = plt.subplots(figsize=(6, 2)) + ax.plot(V_raw_input, lw=1.5, color="cyan") + ax.set_title("Potential Input") + ax.set_xlabel("Grid index") + ax.set_ylabel("Potential") + fig.tight_layout() + return fig + + +def plot_alive(E, psi, V, x, no=1, nos=5, mode=''): + """ + Plot wavefunctions as probability densities with separate energy and probability axes. + + Creates a physically accurate plot showing: + - Potential V(x) and energy levels on left y-axis + - Probability densities |ψ|² on right y-axis (separate scale) + - Color-synchronized between probability curves and energy levels + + Parameters + ---------- + E : ndarray + Energy eigenvalues (in Hartree) + psi : ndarray, shape (N, M) + Wavefunction array where psi[:, i] is the i-th eigenstate + V : ndarray, shape (N+2,) + Full potential array including boundaries + x : ndarray, shape (N+2,) + Full spatial grid including boundaries + no : int, optional + State index to plot if mode != 'all' (default: 1) + nos : int, optional + Number of states to plot if mode == 'all' (default: 5) + mode : str, optional + Plot mode: + - 'all': Plot multiple states (first nos states) + - '': Plot single state (state no) + Default: '' (single state) + + Returns + ------- + fig : matplotlib.figure.Figure + Figure object with dual y-axes + - ax1 (left): Energy/Potential scale + - ax2 (right): Probability density scale + + Notes + ----- + - Uses dark background theme + - Probability densities are plotted as |ψ|², not ψ + - Each state has matching colors for its probability curve and energy level + - Regions where V > 10⁵ are hidden (infinite walls) + + """ + """ + Examples + -------- + >>> # Plot first 5 states + >>> fig = plot_alive(E, psi, V_full, x_full, nos=5, mode='all') + >>> plt.show() + >>> + >>> # Plot only ground state + >>> fig = plot_alive(E, psi, V_full, x_full, no=0) + >>> plt.show() + """ + import matplotlib.pyplot as plt + + plt.style.use("dark_background") + fig, ax1 = plt.subplots(figsize=(10, 6)) + + ax2 = ax1.twinx() # Right axis for probability + + states = min(nos, len(E)) + x_solver = x[1:-1] + V_internal = V[1:-1] + + # --- Plot Potential --- + ax1.plot(x, V, color="white", lw=2, label="V(x)", alpha=0.7) + + # --- Plot wavefunctions --- + if mode == 'all': + for n in range(states): + # 1. Get the synchronized color for this state + color = plt.colormaps["tab20"].colors[n % 20] + + psi_n_sq = psi[:, n]**2 + + # 2. Plot probability density on ax2 with the chosen color + ax2.plot( + x_solver, psi_n_sq, + label=rf"$|\psi_{n}|^2$ (E={E[n]:.2f})", + lw=1.2, + color=color # <-- EXPLICIT COLOR SET + ) + + # 3. Plot energy line on ax1 with the same color + ax1.axhline(E[n], linestyle="--", lw=0.8, alpha=0.5, color=color) # <-- EXPLICIT COLOR SET + else: + # For single state mode, we still need a color. Use 'no' as the index. + n = no + color = plt.colormaps["tab20"].colors[n % 20] + + psi_n_sq = psi[:, n]**2 + + # Plot probability density on ax2 with the chosen color + ax2.plot( + x_solver, psi_n_sq, + label=rf"$|\psi_{no}|^2$ (E={E[no]:.2f})", + lw=1.2, + color=color # <-- EXPLICIT COLOR SET + ) + + # Plot energy line on ax1 with the same color + ax1.axhline(E[no], linestyle="--", lw=0.8, alpha=0.5, color=color) # <-- EXPLICIT COLOR SET + + # Formatting + ax1.set_xlabel("x [a.u.]") + ax1.set_ylabel("Energy / V(x)") + ax2.set_ylabel(r"Probability Density $|\psi|^2$") + + ax1.set_title("Physically Accurate Eigenstates and Potential") + + # The fig.legend() might show the color/style of the *last* plot line. + # For robust legend handling with twinx, you often need to combine the handles. + h1, l1 = ax1.get_legend_handles_labels() + h2, l2 = ax2.get_legend_handles_labels() + ax1.legend(h1+h2, l1+l2, loc="upper right", fontsize=8) # <-- Improved Legend + + plt.tight_layout() + return fig + +def plot_dead(E, psi, V, x, nos=5): + """Textbook: wavefunctions vertically shifted by energy.""" + plt.style.use("dark_background") + fig, (ax_main, ax_bar) = plt.subplots( + 1, 2, figsize=(10, 7), gridspec_kw={"width_ratios": [5, 1]} + ) + fig.subplots_adjust(bottom=0.2, wspace=0.4) + + states = min(nos, len(E)) + x_solver = x[1:-1] + V_internal = V[1:-1] + + if states <= 0: + return fig + + scale = (E[1] - E[0]) * 0.4 if states > 1 else max(E[0] * 0.1, 0.5) + max_E = E[states - 1] + window_height = max_E * 1.5 + + # Plot shifted wavefunctions + for n in range(states): + psi_n = psi[:, n] + maxabs = np.max(np.abs(psi_n)) + psi_norm = psi_n / (maxabs if maxabs != 0 else 1) + y = psi_norm * scale + E[n] + y[V_internal > 1e5] = np.nan # hide where potential is infinite + + color = plt.colormaps["tab20"].colors[n % 20] + ax_main.plot(x_solver, y, lw=1.3, color=color, label=f"n={n+1}, E={E[n]:.2f}") + + # Plot potential + V_clip = np.clip(V, 0, window_height) + ax_main.plot(x, V_clip, color="white", lw=2, label="V(x)") + + ax_main.set_title("Eigenstates + Potential") + ax_main.set_xlabel("x [a.u.]") + ax_main.set_ylabel("Energy / ψ") + ax_main.set_ylim(0, max_E * 1.2) + ax_main.legend(fontsize=8) + + # Energy levels + ax_bar.set_title("Energy Spectrum") + ax_bar.set_xticks([]) + ax_bar.set_ylim(0, np.max(E[:states]) * 1.1) + for n in range(states): + ax_bar.axhline(E[n], lw=1, color=plt.colormaps["tab20"].colors[n % 20]) + + return fig + + +# ========================================== +# 6. BENCHMARKING FUNCTIONS +# ========================================== +def check_ortho(psi, dx, num_states_to_check=20): + """ + Checks the orthonormality of the first 'num_states_to_check' wave functions. + """ + N_CHECK = min(psi.shape[1], num_states_to_check) + overlap_matrix = np.zeros((N_CHECK, N_CHECK)) + + for i in range(N_CHECK): + for j in range(N_CHECK): + # Riemann Sum: integral(psi_i * psi_j) dx + Rsum = np.sum(psi[:, i] * psi[:, j]) * dx + overlap_matrix[i, j] = Rsum + + print(f"\n--- Orthonormality Check (First {N_CHECK} states) ---") + print("Overlap Matrix should approximate the Identity Matrix:") + return overlap_matrix + +def show_matrix(overlap_matrix,how='normal',round_value=10): + ''' + how = normal, round , plot + ''' + if how == 'normal': + print(overlap_matrix[:3]) + elif how == 'round': + print(np.round(overlap_matrix, round_value)) + elif how == 'plot': + plt.figure(figsize=(6,5)) + plt.imshow(overlap_matrix, cmap='coolwarm', origin='lower') + plt.colorbar(label="Overlap Value") + plt.title("Orthonormality Check Matrix") + plt.xlabel("State Index m") + plt.ylabel("State Index n") + plt.gca().invert_yaxis() + plt.locator_params(axis='y', integer=True) + plt.locator_params(axis='x', integer=True) + plt.show() + +def check_ISW_analytic(E, lower_bound=-10, upper_bound=10, hbar=1.0, m=1.0, max_levels=6): + """ + Compares numerical energies to the Infinite Square Well analytic formula. + + Parameters: + ----------- + E : array + Numerical eigenvalues + lower_bound : float + Lower boundary of the well (default: -10) + upper_bound : float + Upper boundary of the well (default: 10) + hbar : float + Reduced Planck constant (default: 1.0) + m : float + Particle mass (default: 1.0) + max_levels : int + Number of levels to check (default: 6) + + """ + """ + Example: + -------- + check_ISW_analytic(E, lower_bound=-10, upper_bound=10) + """ + L = upper_bound - lower_bound # Well width + CHECK_N = min(max_levels, len(E)) + E_numerical = E[:CHECK_N] + E_analytic = np.zeros(CHECK_N) + + for i in range(CHECK_N): + n = i + 1 + E_analytic[i] = (hbar**2 * np.pi**2 * n**2) / (2*m*L**2) + + print("\n### ENERGY BENCHMARK: Infinite Square Well ###") + print(f"Well boundaries: x = [{lower_bound}, {upper_bound}], Width L = {L}") + print("-" * 55) + print(f"| n | Analytic E | Numerical E | % Error |") + print("-" * 55) + + for i in range(CHECK_N): + percent_error = np.abs((E_numerical[i] - E_analytic[i]) / E_analytic[i]) * 100 + print( + f"| {i+1:<1} | {E_analytic[i]:<10.6f} | {E_numerical[i]:<11.6f} | {percent_error:<7.4f}% |" + ) + print("-" * 55) + + return E_analytic, E_numerical + +def check_harmonic_analytic(E, k=None, center=0.0, hbar=1.0, m=1.0, max_levels=6): + """ + Compares numerical energies to the Harmonic Oscillator analytic formula. + + Parameters: + ----------- + E : array + Numerical eigenvalues + k : float, optional + Spring constant. If None, uses Last_k_value global variable + center : float + Center position of the harmonic oscillator (default: 0.0) + hbar : float + Reduced Planck constant (default: 1.0) + m : float + Particle mass (default: 1.0) + max_levels : int + Number of levels to check (default: 6) + + Example: + -------- + check_harmonic_analytic(E, k=10, center=0) + """ + CHECK_N = min(max_levels, len(E)) + + try: + # Use provided k or fall back to global Last_k_value + if k is None: + k = Last_k_value + if k is None: + print("ERROR: k is not set. Please provide k parameter or run harmonic() first.") + return + + w = np.sqrt(k/m) + E_numerical = E[:CHECK_N] + E_analytic = np.zeros(CHECK_N) + + for i in range(CHECK_N): + n_quantum = i + E_analytic[i] = (n_quantum + 0.5) * hbar * w + + print("\n### ENERGY BENCHMARK: Harmonic Oscillator ###") + print(f"Spring constant k = {k}, Center = {center}, omega = {w:.4f}") + print("-" * 55) + print(f"| n | Analytic E | Numerical E | % Error |") + print("-" * 55) + + for i in range(CHECK_N): + n_label = i + percent_error = np.abs((E_numerical[i] - E_analytic[i]) / E_analytic[i]) * 100 + + print( + f"| {n_label:<1} | {E_analytic[i]:<10.6f} | {E_numerical[i]:<11.6f} | {percent_error:<7.4f}% |" + ) + print("-" * 55) + + return E_analytic, E_numerical + + except Exception as e: + print(f"Error in harmonic oscillator check: {e}") + + +def check_finite_well_analytic(E, V0, lower_bound=-10, upper_bound=10, hbar=1.0, m=1.0, max_levels=10): + """ + Compares numerical energies to the Finite Square Well analytical solution. + + The finite square well has no simple closed-form solution, but bound state + energies can be found by solving transcendental equations numerically. + + Parameters: + ----------- + E : array + Numerical eigenvalues from your solver + V0 : float + Barrier height (potential outside the well) + lower_bound : float + Lower boundary of the well (default: -10) + upper_bound : float + Upper boundary of the well (default: 10) + hbar : float + Reduced Planck constant (default: 1.0) + m : float + Particle mass (default: 1.0) + max_levels : int + Maximum number of levels to check (default: 10) + + Example: + -------- + check_finite_well_analytic(E, V0=2.0, lower_bound=-10, upper_bound=10) + """ + a = (upper_bound - lower_bound) / 2 # Half-width + z0 = a * np.sqrt(2 * m * V0) / hbar # Dimensionless parameter + + # Find analytical energies by solving transcendental equations + E_analytic = [] + + # Even parity states: z*tan(z) = sqrt(z0^2 - z^2) + z_vals = np.linspace(0.01, z0 - 0.01, 10000) + for n in range(max_levels): + try: + lhs = z_vals * np.tan(z_vals) + rhs = np.sqrt(z0**2 - z_vals**2) + diff = lhs - rhs + + # Find sign changes (crossings) + for i in range(len(diff) - 1): + if diff[i] * diff[i+1] < 0: + z = z_vals[i] + E_candidate = (hbar**2 * z**2) / (2 * m * a**2) + if E_candidate < V0 and not any(np.isclose(E_candidate, E_a, rtol=1e-3) for E_a in E_analytic): + E_analytic.append(E_candidate) + break + except: + pass + + # Odd parity states: -z*cot(z) = sqrt(z0^2 - z^2) + for n in range(max_levels): + try: + lhs = -z_vals / np.tan(z_vals) + rhs = np.sqrt(z0**2 - z_vals**2) + diff = lhs - rhs + + for i in range(len(diff) - 1): + if diff[i] * diff[i+1] < 0: + z = z_vals[i] + E_candidate = (hbar**2 * z**2) / (2 * m * a**2) + if E_candidate < V0 and not any(np.isclose(E_candidate, E_a, rtol=1e-3) for E_a in E_analytic): + E_analytic.append(E_candidate) + break + except: + pass + + E_analytic = sorted(E_analytic) + + # Filter numerical energies to only bound states + E_numerical_bound = E[E < V0] + + CHECK_N = min(len(E_analytic), len(E_numerical_bound), max_levels) + + if CHECK_N == 0: + print("\n### ENERGY BENCHMARK: Finite Square Well ###") + print(f"Well: x in [{lower_bound}, {upper_bound}], V0 = {V0}, z0 = {z0:.4f}") + print("WARNING: No bound states found!") + print(f" Barrier too shallow. Need V0 > {E[0]:.4f} to bind the ground state.") + return None, None + + print("\n### ENERGY BENCHMARK: Finite Square Well ###") + print(f"Well: x in [{lower_bound}, {upper_bound}], V0 = {V0}, z0 = {z0:.4f}") + print(f"Number of bound states: {CHECK_N}") + print("-" * 55) + print(f"| n | Analytic E | Numerical E | % Error |") + print("-" * 55) + + for i in range(CHECK_N): + percent_error = np.abs((E_numerical_bound[i] - E_analytic[i]) / E_analytic[i]) * 100 + print( + f"| {i:<1} | {E_analytic[i]:<10.6f} | {E_numerical_bound[i]:<11.6f} | {percent_error:<7.4f}% |" + ) + print("-" * 55) + + return np.array(E_analytic[:CHECK_N]), E_numerical_bound[:CHECK_N] + + + + +## +# Verify + +import sys + +def run_comparison(): + """ + Cross-verification: Hand-wave solver vs QMSolve package. + + Compares results for: + 1. Double Well potential + 2. Harmonic Oscillator (debug test) + + Results saved to 'comparison_log.txt' + + Requires + -------- + QMSolve package: pip install qmsolve + + Usage + ----- + >>> from functions import run_comparison + >>> run_comparison() + """ + # Import qmsolve only when this function is called + try: + from qmsolve import Hamiltonian, SingleParticle, init_visualization + except ImportError: + print("Error: qmsolve not found. Please install it via 'pip install qmsolve'") + return + + with open("comparison_log.txt", "w") as log_file: + sys.stdout = log_file + print("========================================") + print("CROSS-VERIFICATION: Hand-wave vs QMSOLVE") + print("========================================") + + # --------------------------------------------------------- + # CASE: Double Well Potential + # V(x) = depth * ( (x-center)**2 - separation )**2 + # --------------------------------------------------------- + print("\n[TEST CASE] Double Well Potential") + + # Parameters + L = 10.0 + N = 512 # QMSolve default is often 512 or similar, let's match + depth = 2.0 + separation = 1.0 + center = 0.0 + m_particle = 1.0 + + print(f"Parameters: L={L}, N={N}, depth={depth}, separation={separation}, m={m_particle}") + + # --------------------------------------------------------- + # 1. Run Hand-wave solver + # --------------------------------------------------------- + print("\n--- Running Hand-wave Solver ---") + x_full, dx, x_internal = make_grid(L=L, N=N) + + # Construct Potential using local V_double_well function + V_internal = V_double_well(x_internal, depth=depth, separation=separation, center=center) + + # Pad for solver + V_full = np.zeros_like(x_full) + V_full[1:-1] = V_internal + V_full[0] = 1e10 + V_full[-1] = 1e10 + + T = kinetic_operator(N, dx, m=m_particle) + E_handwave, psi_handwave = solve(T, V_full, dx) + + print(f"Hand-wave Energies (first 5): {E_handwave[:5]}") + + # --------------------------------------------------------- + # 2. Run QMSolve + # --------------------------------------------------------- + print("\n--- Running QMSolve ---") + + # Define potential function for QMSolve + def double_well(particle): + x = particle.x + return depth * ( (x - center)**2 - separation )**2 + + # Setup QMSolve + H = Hamiltonian(particles = SingleParticle(m = m_particle), + potential = double_well, + spatial_ndim = 1, N = N, extent = L) + + # Diagonalize + eigenstates = H.solve(max_states = 10) + E_qm_eV = eigenstates.energies + + # Convert QMSolve (eV) to Hartree + # 1 Hartree = 27.211386 eV + Hartree_to_eV = 27.211386 + E_qm = E_qm_eV / Hartree_to_eV + + print(f"QMSolve Energies (eV): {E_qm_eV[:5]}") + print(f"QMSolve Energies (Hartree): {E_qm[:5]}") + + # --------------------------------------------------------- + # 3. Compare + # --------------------------------------------------------- + print("\n--- Comparison Results ---") + print("-" * 65) + print(f"| n | Hand-wave E | QMSolve E | Diff | % Diff |") + print("-" * 65) + + for i in range(5): + e1 = E_handwave[i] + e2 = E_qm[i] + diff = abs(e1 - e2) + p_diff = (diff / e2) * 100 if e2 != 0 else 0.0 + + print(f"| {i:<1} | {e1:<12.6f} | {e2:<12.6f} | {diff:<12.2e} | {p_diff:<7.4f}% |") + print("-" * 65) + + # --------------------------------------------------------- + # DEBUG CASE: Harmonic Oscillator + # --------------------------------------------------------- + print("\n[DEBUG CASE] Harmonic Oscillator (k=1)") + k_debug = 1.0 + + # Hand-wave solver + V_internal_HO = 0.5 * k_debug * x_internal**2 + V_full_HO = np.zeros_like(x_full) + V_full_HO[1:-1] = V_internal_HO + V_full_HO[0] = 1e10 + V_full_HO[-1] = 1e10 + + E_handwave_HO, _ = solve(T, V_full_HO, dx) + print(f"Hand-wave HO Energies: {E_handwave_HO[:5]}") + + # QMSolve + def harmonic_potential(particle): + return 0.5 * k_debug * particle.x**2 + + H_HO = Hamiltonian(particles = SingleParticle(m = m_particle), + potential = harmonic_potential, + spatial_ndim = 1, N = N, extent = L) + eigenstates_HO = H_HO.solve(max_states = 10) + E_qm_HO = eigenstates_HO.energies + print(f"QMSolve HO Energies: {E_qm_HO[:5]}") + + sys.stdout = sys.__stdout__ + print("\n✓ Comparison complete! Results saved to 'comparison_log.txt'") + + +# ========================================== +# NOTEBOOK-FRIENDLY VERIFICATION FUNCTIONS +# ========================================== + +def verify_qmsolve(E_your=None, psi_your=None, V_your=None, x_your=None, + potential_type='double_well', potential_params=None): + """ + QMSolve comparison using YOUR notebook variables. + + Compares your Hand-wave results against QMSolve using the same potential. + + Parameters + ---------- + E_your : ndarray, optional + Your computed energy eigenvalues + If None, will compute using default double well + psi_your : ndarray, optional + Your computed wavefunctions + V_your : ndarray, optional + Your potential array (full, including boundaries) + x_your : ndarray, optional + Your spatial grid (full, including boundaries) + potential_type : str, optional + Type of potential: 'double_well', 'harmonic', 'custom' + Default: 'double_well' + potential_params : dict, optional + Parameters for the potential, e.g.: + {'depth': 2.0, 'separation': 1.0, 'center': 0.0} for double_well + {'k': 1.0, 'center': 0.0} for harmonic + + Usage in notebook + ----------------- + # After you've computed E, psi, V, x in your notebook: + >>> verify_qmsolve(E_your=E, psi_your=psi, V_your=V_full, x_your=x, + ... potential_type='double_well', + ... potential_params={'depth': 2.0, 'separation': 1.0, 'center': 0.0}) + + # Or use defaults: + >>> verify_qmsolve() + """ + try: + from qmsolve import Hamiltonian, SingleParticle + except ImportError: + print("❌ Error: qmsolve not found.") + print("Install with: pip install qmsolve") + return + + print("="*70) + print("CROSS-VERIFICATION: Your Results vs QMSolve") + print("="*70) + + # Use provided values or compute defaults + if E_your is None or x_your is None: + print("\n⚠️ No input provided. Using default Double Well test case.") + + # Default parameters + L = 10.0 + N = 512 + if potential_params is None: + potential_params = {'depth': 2.0, 'separation': 1.0, 'center': 0.0} + + print(f"\n[TEST] {potential_type.replace('_', ' ').title()}") + print(f"Parameters: L={L}, N={N}, {potential_params}") + + # Compute using Hand-wave + x_your, dx, x_internal = make_grid(L=L, N=N) + + if potential_type == 'double_well': + V_internal = V_double_well(x_internal, **potential_params) + elif potential_type == 'harmonic': + V_internal = harmonic(x_internal, **potential_params) + else: + print("❌ Unknown potential type") + return + + V_your = np.zeros_like(x_your) + V_your[1:-1] = V_internal + V_your[0] = 1e10 + V_your[-1] = 1e10 + + T = kinetic_operator(N, dx) + E_your, psi_your = solve(T, V_your, dx) + else: + # Use provided values + print(f"\n✓ Using your computed results") + print(f" Grid points: {len(x_your)}") + print(f" Domain: [{x_your[0]:.2f}, {x_your[-1]:.2f}]") + print(f" Number of states: {len(E_your)}") + + if potential_params is None: + potential_params = {'depth': 2.0, 'separation': 1.0, 'center': 0.0} + + L = x_your[-1] - x_your[0] + N = len(x_your) - 2 # Internal points + + print(f"\n--- Your Hand-wave Results ---") + print(f"Energies (first 5): {E_your[:5]}") + + # Run QMSolve with same parameters + print(f"\n--- Running QMSolve with same potential ---") + + # Define potential function for QMSolve + if potential_type == 'double_well': + depth = potential_params.get('depth', 2.0) + separation = potential_params.get('separation', 1.0) + center = potential_params.get('center', 0.0) + + def potential_func(particle): + x = particle.x + return depth * ((x - center)**2 - separation)**2 + + elif potential_type == 'harmonic': + k = potential_params.get('k', 1.0) + center = potential_params.get('center', 0.0) + + def potential_func(particle): + return 0.5 * k * (particle.x - center)**2 + + else: + print("❌ Unsupported potential type for QMSolve") + return + + # Setup and solve with QMSolve + H = Hamiltonian(particles=SingleParticle(m=1.0), + potential=potential_func, + spatial_ndim=1, N=N, extent=L) + + eigenstates = H.solve(max_states=min(10, len(E_your))) + E_qm_eV = eigenstates.energies + + # Convert to Hartree + Hartree_to_eV = 27.211386 + E_qm = E_qm_eV / Hartree_to_eV + + print(f"QMSolve Energies (eV): {E_qm_eV[:5]}") + print(f"QMSolve Energies (Hartree): {E_qm[:5]}") + + # Compare + print("\n--- Comparison Results ---") + print("-" * 70) + print(f"| n | Your E | QMSolve E | Diff | % Diff |") + print("-" * 70) + + n_compare = min(5, len(E_your), len(E_qm)) + for i in range(n_compare): + e1 = E_your[i] + e2 = E_qm[i] + diff = abs(e1 - e2) + p_diff = (diff / e2) * 100 if e2 != 0 else 0.0 + print(f"| {i:<1} | {e1:<12.6f} | {e2:<12.6f} | {diff:<12.2e} | {p_diff:<7.4f}% |") + + print("-" * 70) + + # Summary + avg_diff = np.mean([abs(E_your[i] - E_qm[i])/E_qm[i]*100 for i in range(n_compare)]) + max_diff = np.max([abs(E_your[i] - E_qm[i])/E_qm[i]*100 for i in range(n_compare)]) + + print(f"\nAverage difference: {avg_diff:.4f}%") + print(f"Maximum difference: {max_diff:.4f}%") + + if max_diff < 0.5: + print("✅ EXCELLENT: Your solver matches QMSolve within 0.5%!") + elif max_diff < 1.0: + print("✅ GOOD: Your solver matches QMSolve within 1%") + else: + print("⚠️ WARNING: Difference > 1%. Check your implementation.") + + print("\n✅ QMSolve verification complete!") + + +def verify_physics(): + """ + Comprehensive physics tests that print directly (no file output). + + Tests: + 1. Infinite Square Well + 2. Harmonic Oscillator + 3. Orthonormality + + Usage in notebook: + >>> from functions import verify_physics + >>> verify_physics() + """ + print("="*70) + print("PHYSICS VERIFICATION") + print("="*70) + + # Test 1: Infinite Square Well + print("\n[TEST 1] Infinite Square Well") + print("-"*70) + L = 20.0 + N = 1000 + x_full, dx, x_internal = make_grid(L=L, N=N) + + V_full = np.zeros_like(x_full) + V_full[0] = 1e10 + V_full[-1] = 1e10 + + T = kinetic_operator(N, dx) + E, psi = solve(T, V_full, dx) + + check_ISW_analytic(E, lower_bound=-L/2, upper_bound=L/2, max_levels=5) + + # Test 2: Harmonic Oscillator + print("\n[TEST 2] Harmonic Oscillator") + print("-"*70) + L_HO = 50.0 + N_HO = 2000 + x_full, dx, x_internal = make_grid(L=L_HO, N=N_HO) + + k = 1.0 + V_internal = harmonic(x_internal, k=k) + + V_full = np.zeros_like(x_full) + V_full[1:-1] = V_internal + V_full[0] = 1e10 + V_full[-1] = 1e10 + + T = kinetic_operator(N_HO, dx) + E, psi = solve(T, V_full, dx) + + check_harmonic_analytic(E, k=k, max_levels=5) + + # Test 3: Orthonormality + print("\n[TEST 3] Orthonormality") + print("-"*70) + overlap = check_ortho(psi, dx, num_states_to_check=5) + + max_off_diag = np.max(np.abs(overlap - np.eye(len(overlap)))) + print(f"Max off-diagonal element: {max_off_diag:.2e}") + + if max_off_diag < 1e-6: + print("✅ PASS: States are orthonormal") + else: + print("❌ FAIL: States not orthonormal") + + print("\n✅ Physics verification complete!") + + +def verify_all(): + """ + Run all verifications (prints directly, no files). + + Usage in notebook: + >>> from functions import verify_all + >>> verify_all() + """ + print("\n" + "="*70) + print("COMPLETE SOLVER VALIDATION") + print("="*70) + + # Run physics tests + verify_physics() + + print("\n") + + # Run QMSolve comparison + verify_qmsolve() + + print("\n" + "="*70) + print("✅ ALL VALIDATIONS COMPLETE!") + print("="*70) + + +def verify_solver(): + """ + Comprehensive verification of Hand-wave solver. + + Tests three fundamental potentials against analytical solutions: + 1. Infinite Square Well (Particle in a Box) + 2. Finite Square Well + 3. Harmonic Oscillator + + Prints all results directly to notebook (no files created). + + Usage in notebook + ----------------- + >>> from functions import verify_solver + >>> verify_solver() + """ + print("\n" + "="*80) + print(" "*20 + "HAND-WAVE SOLVER VERIFICATION") + print("="*80) + print("\nTesting against analytical solutions for fundamental quantum systems") + print("-"*80) + + # ======================================== + # TEST 1: Infinite Square Well + # ======================================== + print("\n" + "="*80) + print("[TEST 1] INFINITE SQUARE WELL (Particle in a Box)") + print("="*80) + + L_isw = 20.0 + N_isw = 1000 + print(f"Domain: L = {L_isw} a.u., Grid points: N = {N_isw}") + + x_isw, dx_isw, x_int_isw = make_grid(L=L_isw, N=N_isw) + + V_isw = np.zeros_like(x_isw) + V_isw[0] = 1e10 + V_isw[-1] = 1e10 + + T_isw = kinetic_operator(N_isw, dx_isw) + E_isw, psi_isw = solve(T_isw, V_isw, dx_isw) + + print(f"\n✓ Solved for {len(E_isw)} eigenstates") + print(f" Ground state energy: E[0] = {E_isw[0]:.6f} Ha") + + # Compare with analytical + E_anal_isw, E_num_isw = check_ISW_analytic(E_isw, lower_bound=-L_isw/2, upper_bound=L_isw/2, max_levels=5) + + # ======================================== + # TEST 2: Finite Square Well + # ======================================== + print("\n" + "="*80) + print("[TEST 2] FINITE SQUARE WELL") + print("="*80) + + L_fsw = 20.0 + N_fsw = 1000 + V0_fsw = 2.0 # Deep well for bound states + + print(f"Domain: L = {L_fsw} a.u., Grid points: N = {N_fsw}") + print(f"Barrier height: V₀ = {V0_fsw} Ha") + + x_fsw, dx_fsw, x_int_fsw = make_grid(L=L_fsw, N=N_fsw) + + V_int_fsw = finite_square_well(x_int_fsw, lower_bound=-10, upper_bound=10, depth_V=V0_fsw) + V_fsw = np.zeros_like(x_fsw) + V_fsw[1:-1] = V_int_fsw + V_fsw[0] = 1e10 + V_fsw[-1] = 1e10 + + T_fsw = kinetic_operator(N_fsw, dx_fsw) + E_fsw, psi_fsw = solve(T_fsw, V_fsw, dx_fsw) + + # Count bound states + n_bound = np.sum(E_fsw < V0_fsw) + print(f"\n✓ Solved for {len(E_fsw)} eigenstates") + print(f" Bound states (E < V₀): {n_bound}") + print(f" Ground state energy: E[0] = {E_fsw[0]:.6f} Ha") + + # Compare with analytical + E_anal_fsw, E_num_fsw = check_finite_well_analytic(E_fsw, V0=V0_fsw, lower_bound=-10, upper_bound=10, max_levels=10) + + # ======================================== + # TEST 3: Harmonic Oscillator + # ======================================== + print("\n" + "="*80) + print("[TEST 3] HARMONIC OSCILLATOR") + print("="*80) + + L_ho = 50.0 + N_ho = 2000 + k_ho = 1.0 + + print(f"Domain: L = {L_ho} a.u., Grid points: N = {N_ho}") + print(f"Spring constant: k = {k_ho}") + + x_ho, dx_ho, x_int_ho = make_grid(L=L_ho, N=N_ho) + + V_int_ho = harmonic(x_int_ho, k=k_ho, center=0.0) + V_ho = np.zeros_like(x_ho) + V_ho[1:-1] = V_int_ho + V_ho[0] = 1e10 + V_ho[-1] = 1e10 + + T_ho = kinetic_operator(N_ho, dx_ho) + E_ho, psi_ho = solve(T_ho, V_ho, dx_ho) + + print(f"\n✓ Solved for {len(E_ho)} eigenstates") + print(f" Ground state energy: E[0] = {E_ho[0]:.6f} Ha") + print(f" Expected (analytical): E[0] = 0.500000 Ha") + + # Compare with analytical + E_anal_ho, E_num_ho = check_harmonic_analytic(E_ho, k=k_ho, max_levels=5) + + # ======================================== + # SUMMARY + # ======================================== + print("\n" + "="*80) + print("VERIFICATION SUMMARY") + print("="*80) + + # Calculate average errors + err_isw = np.mean(np.abs((E_num_isw - E_anal_isw) / E_anal_isw) * 100) + err_ho = np.mean(np.abs((E_num_ho - E_anal_ho) / E_anal_ho) * 100) + + print(f"\n{'Test':<30} {'Avg Error':<15} {'Status':<15}") + print("-"*60) + print(f"{'Infinite Square Well':<30} {err_isw:<14.4f}% {'✅ PASS' if err_isw < 0.01 else '⚠️ CHECK':<15}") + print(f"{'Harmonic Oscillator':<30} {err_ho:<14.4f}% {'✅ PASS' if err_ho < 0.02 else '⚠️ CHECK':<15}") + + if E_anal_fsw is not None: + err_fsw = np.mean(np.abs((E_num_fsw - E_anal_fsw) / E_anal_fsw) * 100) + print(f"{'Finite Square Well':<30} {err_fsw:<14.4f}% {'✅ PASS' if err_fsw < 0.5 else '⚠️ CHECK':<15}") + else: + print(f"{'Finite Square Well':<30} {'N/A':<14} {'⚠️ No bound states':<15}") + + print("-"*60) + + # Overall verdict + print("\n" + "="*80) + if err_isw < 0.01 and err_ho < 0.02: + print("✅ VERIFICATION PASSED: Solver is accurate and validated!") + else: + print("⚠️ VERIFICATION WARNING: Check solver implementation") + print("="*80) + print() + + + +# ========================================== +# VERIFICATION FUNCTION FOR NOTEBOOKS +# ========================================== + +def run_verification(): + """ + Comprehensive physics verification tests. + + Tests multiple potentials against analytical solutions: + 1. Infinite Square Well + 2. Harmonic Oscillator + 3. Half-Harmonic Oscillator + 4. Triangular Potential + 5. Hamiltonian Construction Verification + + Results are saved to 'verification_log.txt' + + Usage + ----- + >>> from functions import run_verification + >>> run_verification() + """ + import sys + + with open("verification_log.txt", "w") as log_file: + sys.stdout = log_file + print("========================================") + print("PHYSICS ENGINE VERIFICATION") + print("========================================") + + # 1. Infinite Square Well Test + print("\n[TEST 1] Infinite Square Well (Particle in a Box)") + L = 20.0 + N = 1000 + x_full, dx, x_internal = make_grid(L=L, N=N) + + V_full = np.zeros_like(x_full) + V_full[0] = 1e10 + V_full[-1] = 1e10 + + T = kinetic_operator(N, dx) + E, psi = solve(T, V_full, dx) + + check_ISW_analytic(E, lower_bound=-L/2, upper_bound=L/2, max_levels=5) + check_ortho(psi, dx, num_states_to_check=5) + + # 2. Harmonic Oscillator Test + print("\n[TEST 2] Harmonic Oscillator") + L_HO = 50.0 + N_HO = 2000 + x_full, dx, x_internal = make_grid(L=L_HO, N=N_HO) + + k = 1.0 + V_internal = harmonic(x_internal, k=k) + + V_full = np.zeros_like(x_full) + V_full[1:-1] = V_internal + V_full[0] = 1e10 + V_full[-1] = 1e10 + + T = kinetic_operator(N_HO, dx) + E, psi = solve(T, V_full, dx) + + check_harmonic_analytic(E, k=k, max_levels=5) + + # 3. Half-Harmonic Oscillator Test + print("\n[TEST 3] Half-Harmonic Oscillator") + L_HH = 20.0 + N_HH = 1000 + x_full, dx, x_internal = make_grid(L=L_HH, N=N_HH) + + k = 1.0 + V_internal = 0.5 * k * x_internal**2 + V_internal[x_internal <= 0] = 1e10 + + V_full = np.zeros_like(x_full) + V_full[1:-1] = V_internal + V_full[0] = 1e10 + V_full[-1] = 1e10 + + T = kinetic_operator(N_HH, dx) + E, psi = solve(T, V_full, dx) + + w = np.sqrt(k/1.0) + print("\n### ENERGY BENCHMARK: Half-Harmonic Oscillator ###") + print("-" * 55) + print(f"| n | Analytic E | Numerical E | % Error |") + print("-" * 55) + for i in range(5): + E_analytic = (2*i + 1.5) * 1.0 * w + percent_error = np.abs((E[i] - E_analytic) / E_analytic) * 100 + print(f"| {i:<1} | {E_analytic:<10.6f} | {E[i]:<11.6f} | {percent_error:<7.4f}% |") + print("-" * 55) + + # 4. Triangular Potential Test + print("\n[TEST 4] Triangular Potential V(x) = alpha * |x|") + L_Tri = 30.0 + N_Tri = 2000 + x_full, dx, x_internal = make_grid(L=L_Tri, N=N_Tri) + + alpha = 1.0 + V_internal = alpha * np.abs(x_internal) + + V_full = np.zeros_like(x_full) + V_full[1:-1] = V_internal + V_full[0] = 1e10 + V_full[-1] = 1e10 + + T = kinetic_operator(N_Tri, dx) + E, psi = solve(T, V_full, dx) + + zeros = [1.01879, 2.33811, 3.24820, 4.08795, 4.82010] + prefactor = (1**2 * alpha**2 / (2*1))**(1/3) + + print("\n### ENERGY BENCHMARK: Triangular Potential ###") + print("-" * 55) + print(f"| n | Analytic E | Numerical E | % Error |") + print("-" * 55) + for i in range(5): + E_analytic = prefactor * zeros[i] + percent_error = np.abs((E[i] - E_analytic) / E_analytic) * 100 + print(f"| {i:<1} | {E_analytic:<10.6f} | {E[i]:<11.6f} | {percent_error:<7.4f}% |") + print("-" * 55) + + # 5. Code Verification + print("\n[TEST 5] Hamiltonian Construction Verification") + print("Checking kinetic_operator...") + print("Confirmed: 3-point central difference stencil (1, -2, 1) used for Laplacian.") + print("Confirmed: Pre-factor -hbar^2/(2m) applied correctly.") + + sys.stdout = sys.__stdout__ + print("\n✓ Verification complete! Results saved to 'verification_log.txt'") + + +## + + + + + +def display_params(frame, params_list, start_y=80, line_height=25, color=(255, 255, 255)): + import cv2 + for i, text in enumerate(params_list): + y = start_y + i * line_height + cv2.putText(frame, text, (10, y), cv2.FONT_HERSHEY_SIMPLEX, + 0.6, (0, 0, 0), 3) + cv2.putText(frame, text, (10, y), cv2.FONT_HERSHEY_SIMPLEX, + 0.6, color, 2) + + +# --------------------------------------------------------------------- +# MAIN FUNCTION: HAND-CONTROLLED POTENTIAL CAPTURE +# --------------------------------------------------------------------- + +# --------------------------------------------------------------------- +# INITIALIZATION +# --------------------------------------------------------------------- + + + +def capture_potential(tune, A_MIN, A_MAX, mode='wait'): + import cv2 + import mediapipe as mp + + mp_hands = mp.solutions.hands + hands = mp_hands.Hands(max_num_hands=2, min_detection_confidence=0.7) + drawer = mp.solutions.drawing_utils + + cap = cv2.VideoCapture(0) + captured_V = None + + # Stability tracking ----------------------------------------------- + stability_counter = 0 + REQUIRED_STABLE_FRAMES = 45 + MOVEMENT_THRESHOLD = 0.015 + prev_landmarks = [] + + # Landmark indices -------------------------------------------------- + THUMB_TIP_ID = 4 + INDEX_TIP_ID = 8 + + # QHO Mapping constants -------------------------------------------- + D_MIN = 0.001 + D_MAX = 0.2 + + D_RANGE = D_MAX - D_MIN + A_RANGE = A_MAX - A_MIN + + SLOPE = -A_RANGE / D_RANGE + INTERCEPT = A_MAX - SLOPE * D_MIN + + # Fixed visual scale (independent of physics range) + PLOT_CEILING_A = 10.0 + EPS = 1e-9 + + print("Controls: HOLD STILL to capture, or press 'q' to quit.") + + # ================================================================= + # MAIN LOOP + # ================================================================= + while True: + ret, frame = cap.read() + if not ret: + break + + frame = cv2.flip(frame, 1) + h, w, _ = frame.shape + + rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + res = hands.process(rgb) + + pot_profile = None + mode_msg = "No Hands" + params_to_display = [] + current_landmarks_flat = [] + + # -------------------------------------------------------------- + # LANDMARK PROCESSING + # -------------------------------------------------------------- + if res.multi_hand_landmarks: + + # Flatten positions for stability detection + for hand_lms in res.multi_hand_landmarks: + for lm in hand_lms.landmark: + current_landmarks_flat.extend([lm.x, lm.y]) + + # Draw detected hands + for lm in res.multi_hand_landmarks: + drawer.draw_landmarks(frame, lm, mp_hands.HAND_CONNECTIONS) + + # ---------------------------------------------------------- + # TWO HANDS = SQUARE WELL (AUTO-CENTERED) + # ---------------------------------------------------------- + if len(res.multi_hand_landmarks) >= 2: + mode_msg = "Mode: Square Well (Auto-Centered)" + + # 1. Get Hand Positions + x_coords = [ + lm.landmark[INDEX_TIP_ID].x * w + for lm in res.multi_hand_landmarks + ] + x_coords.sort() + xL_hand, xR_hand = int(x_coords[0]), int(x_coords[1]) + + # 2. Draw Yellow lines at REAL hand positions (Visual Feedback) + cv2.line(frame, (xL_hand, 0), (xL_hand, h), (0, 255, 255), 2) + cv2.line(frame, (xR_hand, 0), (xR_hand, h), (0, 255, 255), 2) + + # 3. Calculate Force-Centered Coordinates + # We calculate the width of your hands, but ignore their position + well_width = xR_hand - xL_hand + center_screen = w / 2 + + # Create boundaries centered on the screen + centered_L = center_screen - (well_width / 2) + centered_R = center_screen + (well_width / 2) + + params_to_display.append(f"Width: {well_width:4.0f} px") + params_to_display.append(f"Status: Centered") + + # 4. Generate Potential (Centered) + x_space = np.linspace(0, w, 400) + pot_profile = np.ones_like(x_space) + # Use centered_L/R instead of hand positions + pot_profile[(x_space > centered_L) & (x_space < centered_R)] = 0 + + """ + # 5. Visualize the Centered Potential (Red Line) + display_pts = np.column_stack(( + x_space, + pot_profile * (h - 10) # simple scaling for viz + )).astype(np.int32) + cv2.polylines(frame, [display_pts], False, (0, 0, 255), 2) + """ + # ---------------------------------------------------------- + # ONE HAND = PINCH PARABOLA (QHO) + # ---------------------------------------------------------- + elif len(res.multi_hand_landmarks) == 1: + mode_msg = "Mode: Pinch QHO" + lm = res.multi_hand_landmarks[0] + + thumb = lm.landmark[THUMB_TIP_ID] + index = lm.landmark[INDEX_TIP_ID] + + dx = index.x - thumb.x + dy = index.y - thumb.y + pinch_distance = math.sqrt(dx**2 + dy**2) + + # Compute curvature + A = SLOPE * pinch_distance + INTERCEPT + A = max(A_MIN, min(A_MAX, A)) + + # This is already mathematically centered at 0 + x_space = np.linspace(-1, 1, 400) + pot_profile = A * (x_space**2) + + # Fixed visual scale + pot_profile = pot_profile / (PLOT_CEILING_A + EPS) + pot_profile = np.clip(pot_profile, 0.0, 1.0) + + params_to_display.append(f"Pinch Dist: {pinch_distance:.4f}") + params_to_display.append(f"A (curv): {A:.4f}") + + display_pts = np.column_stack(( + (x_space + 1)/2 * w, + (1 - pot_profile) * h + )).astype(np.int32) + + cv2.polylines(frame, [display_pts], False, (0, 0, 255), 2) + + # ============================================================== + # STABILITY CHECK + # ============================================================== + if mode != 'wait': + if current_landmarks_flat and prev_landmarks: + if len(current_landmarks_flat) == len(prev_landmarks): + movement = np.mean(np.abs( + np.array(current_landmarks_flat) + - np.array(prev_landmarks) + )) + if movement < MOVEMENT_THRESHOLD: + stability_counter += 1 + else: + stability_counter = 0 + else: + stability_counter = 0 + else: + stability_counter = 0 + + prev_landmarks = current_landmarks_flat + + # Show loading bar + if stability_counter > 0: + progress = stability_counter / REQUIRED_STABLE_FRAMES + bar_width = int(w * progress) + color = (0, 255*progress, 255*(1-progress)) + cv2.rectangle(frame, (0, 0), (bar_width, 20), color, -1) + cv2.putText(frame, "HOLDING...", (10, 15), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) + + # Finished + if stability_counter >= REQUIRED_STABLE_FRAMES and pot_profile is not None: + captured_V = pot_profile + frame[:] = 255 + cv2.imshow("Quantum Potential Input", frame) + cv2.waitKey(100) + print("Stable capture triggered!") + break + + # -------------------------------------------------------------- + # UI OVERLAY + # -------------------------------------------------------------- + cv2.putText(frame, mode_msg, (10, 50), + cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) + + display_params(frame, params_to_display) + cv2.imshow("Quantum Potential Input", frame) + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + # ----------------------------------------------------------------- + cap.release() + cv2.destroyAllWindows() + return captured_V + +# Create a notebook-friendly version of the function +def cheese(tune, A_MIN, A_MAX, mode='wait'): + import time + from IPython.display import display, Image, clear_output + + + # Copy relevant constants from the file for local scope + THUMB_TIP_ID = 4 + INDEX_TIP_ID = 8 + REQUIRED_STABLE_FRAMES = 45 + MOVEMENT_THRESHOLD = 0.015 + PLOT_CEILING_A = 10.0 + EPS = 1e-9 + + D_MIN = 0.001 + D_MAX = 0.2 + D_RANGE = D_MAX - D_MIN + A_RANGE = A_MAX - A_MIN + SLOPE = -A_RANGE / D_RANGE + INTERCEPT = A_MAX - SLOPE * D_MIN + # End of copied constants + + cap = cv2.VideoCapture(0) + captured_V = None + + if not cap.isOpened(): + print("Error: Could not open video stream. Check permissions or camera index.") + return None + + stability_counter = 0 + prev_landmarks = [] + + start_time = time.time() + MAX_RUN_TIME_SECONDS = 30 + + print("Controls: HOLD STILL to capture, or wait for the time limit to exit.") + + while True: + ret, frame = cap.read() + if not ret: + break + + frame = cv2.flip(frame, 1) + h, w, _ = frame.shape + rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + res = hands.process(rgb) + + pot_profile = None + mode_msg = "No Hands" + params_to_display = [] + current_landmarks_flat = [] + + # --- LANDMARK AND POTENTIAL LOGIC (Skipped for brevity, assume this is correct) --- + if res.multi_hand_landmarks: + for hand_lms in res.multi_hand_landmarks: + for lm in hand_lms.landmark: + current_landmarks_flat.extend([lm.x, lm.y]) + for lm in res.multi_hand_landmarks: + drawer.draw_landmarks(frame, lm, mp_hands.HAND_CONNECTIONS) + + # TWO HANDS (Square Well) + if len(res.multi_hand_landmarks) >= 2: + mode_msg = "Mode: Square Well (Auto-Centered)" + x_coords = [lm.landmark[INDEX_TIP_ID].x * w for lm in res.multi_hand_landmarks] + x_coords.sort() + xL_hand, xR_hand = int(x_coords[0]), int(x_coords[1]) + cv2.line(frame, (xL_hand, 0), (xL_hand, h), (0, 255, 255), 2) + cv2.line(frame, (xR_hand, 0), (xR_hand, h), (0, 255, 255), 2) + well_width = xR_hand - xL_hand + center_screen = w / 2 + centered_L = center_screen - (well_width / 2) + centered_R = center_screen + (well_width / 2) + params_to_display.append(f"Width: {well_width:4.0f} px") + params_to_display.append(f"Status: Centered") + x_space = np.linspace(0, w, 400) + pot_profile = np.ones_like(x_space) + pot_profile[(x_space > centered_L) & (x_space < centered_R)] = 0 + # ONE HAND (QHO) + elif len(res.multi_hand_landmarks) == 1: + mode_msg = "Mode: Pinch QHO" + lm = res.multi_hand_landmarks[0] + thumb = lm.landmark[THUMB_TIP_ID] + index = lm.landmark[INDEX_TIP_ID] + dx = index.x - thumb.x + dy = index.y - thumb.y + pinch_distance = math.sqrt(dx**2 + dy**2) + A = SLOPE * pinch_distance + INTERCEPT + A = max(A_MIN, min(A_MAX, A)) + x_space = np.linspace(-1, 1, 400) + pot_profile = A * (x_space**2) + pot_profile = pot_profile / (PLOT_CEILING_A + EPS) + pot_profile = np.clip(pot_profile, 0.0, 1.0) + params_to_display.append(f"Pinch Dist: {pinch_distance:.4f}") + params_to_display.append(f"A (curv): {A:.4f}") + display_pts = np.column_stack(((x_space + 1)/2 * w, (1 - pot_profile) * h)).astype(np.int32) + cv2.polylines(frame, [display_pts], False, (0, 0, 255), 2) + # --- END LANDMARK AND POTENTIAL LOGIC --- + + # STABILITY CHECK + if mode != 'wait': + if current_landmarks_flat and prev_landmarks and len(current_landmarks_flat) == len(prev_landmarks): + movement = np.mean(np.abs(np.array(current_landmarks_flat) - np.array(prev_landmarks))) + stability_counter = stability_counter + 1 if movement < MOVEMENT_THRESHOLD else 0 + else: + stability_counter = 0 + + prev_landmarks = current_landmarks_flat + + if stability_counter > 0: + progress = stability_counter / REQUIRED_STABLE_FRAMES + bar_width = int(w * progress) + color = (0, 255*progress, 255*(1-progress)) + cv2.rectangle(frame, (0, 0), (bar_width, 20), color, -1) + cv2.putText(frame, "HOLDING...", (10, 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) + + # Finished + if stability_counter >= REQUIRED_STABLE_FRAMES and pot_profile is not None: + captured_V = pot_profile + cap.release() + # --- LINE REMOVED HERE (was cv2.destroyAllWindows()) --- + print("Stable capture triggered and video stream closed.") + return captured_V + + # UI OVERLAY + cv2.putText(frame, mode_msg, (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) + display_params(frame, params_to_display) + + # NOTEBOOK DISPLAY + clear_output(wait=True) + _, buffer = cv2.imencode('.jpeg', frame) + display(Image(data=buffer.tobytes())) + + time.sleep(0.01) + + if time.time() - start_time > MAX_RUN_TIME_SECONDS: + print(f"Time limit of {MAX_RUN_TIME_SECONDS} seconds reached.") + break + + # ----------------------------------------------------------------- + cap.release() + return captured_V + + + + + +### +import qrcode +from IPython.display import display, Image + +def show_QR(url): + # The file name to save the QR code image + file_name = "hand_wave_link_qrcode.png" + + # --- QR Code Generation --- + # 1. Create a QR code object with specific settings + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + + # 2. Add the URL data to the object + qr.add_data(url) + qr.make(fit=True) + + # 3. Create the QR code image + img = qr.make_image(fill_color="black", back_color="white") + + # 4. Save the image to the local directory + img.save(file_name) + + # --- Display in Jupyter Notebook --- + + # 5. Display the saved image using IPython.display + return display(Image(filename=file_name)) + + + + +