Hand-wave / functions.py
AhiBucket's picture
Upload functions.py with huggingface_hub
bc44f08 verified
# 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))