Hand-wave / verify_physics.py
Ahilan Kumaresan
Add all files from psi_solve2: notebooks, verification scripts, and logs
794cdea
import numpy as np
import psi_solve2.functions as f
import sys
def run_verification():
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 = f.make_grid(L=L, N=N)
# Define potential: Infinite walls at -L/2 and L/2 are implicit in the solver boundary conditions
# But we can use the helper to be explicit or just 0 inside
# The solver assumes V=infinity at boundaries of the grid if we just solve for internal points
# Let's use a slightly smaller box to test the potential function if needed,
# but for standard ISW matching the grid size:
V_full = np.zeros_like(x_full)
# The make_grid creates x from -L/2 to L/2.
# The solver solves for points 1 to N-1 (internal).
# So effectively the walls are at x[0] and x[-1].
T = f.kinetic_operator(N, dx)
E, psi = f.solve(T, V_full, dx)
f.check_ISW_analytic(E, L=L, max_levels=5)
f.check_ortho(psi, dx, num_states_to_check=5)
# 2. Harmonic Oscillator Test
print("\n[TEST 2] Harmonic Oscillator")
# Use a larger box for HO to ensure wavefunction decays to 0 before walls
L_HO = 50.0
N_HO = 2000
x_full, dx, x_internal = f.make_grid(L=L_HO, N=N_HO)
k = 1.0
# Note: functions.harmonic sets a global variable 'Last_k_value' which is needed for the check function
V_internal = f.harmonic(x_internal, k=k)
# Pad V to match full grid size for solve function if it expects full V?
# functions.solve takes V_full and slices it: V_internal = V_full[1:-1]
# So we need to construct V_full
V_full = np.zeros_like(x_full)
V_full[1:-1] = V_internal
V_full[0] = 1e10 # Wall
V_full[-1] = 1e10 # Wall
T = f.kinetic_operator(N_HO, dx)
E, psi = f.solve(T, V_full, dx)
f.check_harmonic_analytic(E, max_levels=5)
# 3. Half-Harmonic Oscillator Test
print("\n[TEST 3] Half-Harmonic Oscillator")
# V(x) = 0.5*k*x^2 for x>0, infinity for x<=0
# We can simulate this by putting a wall at x=0
L_HH = 20.0
N_HH = 1000
x_full, dx, x_internal = f.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 # Wall at x=0
V_full = np.zeros_like(x_full)
V_full[1:-1] = V_internal
V_full[0] = 1e10
V_full[-1] = 1e10
T = f.kinetic_operator(N_HH, dx)
E, psi = f.solve(T, V_full, dx)
# Analytic: E_n = hbar * w * (2n + 1.5) for n=0,1,2... (odd states of full harmonic)
# Or just odd states of full harmonic: E_1, E_3, E_5... => 1.5, 3.5, 5.5...
w = np.sqrt(k/1.0) # m=1
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 = f.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 = f.kinetic_operator(N_Tri, dx)
E, psi = f.solve(T, V_full, dx)
# Analytic: E_n = (hbar^2 * alpha^2 / (2m))^(1/3) * a_n
# where a_n are zeros of Airy function derivative (even states) and Airy function (odd states)
# Actually, for V = alpha*|x|, the energies are related to the negative zeros of the Airy function Ai(-z)
# E_n = (hbar^2 * alpha^2 / (2m))^(1/3) * |z_n|
# Zeros of Ai(-z): 2.338, 4.088, 5.521, 6.787, 7.944 ...
# Wait, standard result:
# E_n approx (hbar^2 alpha^2 / 2m)^(1/3) * [3/2 pi (n + 1/2)]^(2/3) (WKB)
# Exact zeros of Ai(-z) are for odd parity states? No, let's use the known values.
# For 1D linear potential V = alpha*|x|:
# The eigenvalues correspond to the zeros of Ai(-E_scaled) for odd parity
# and Ai'(-E_scaled) for even parity.
# Let's use pre-calculated zeros for the first few states.
# Zeros of Ai'(z) (even states): -1.0188, -3.2482, -4.8201...
# Zeros of Ai(z) (odd states): -2.3381, -4.0879, -5.5206...
# Sorted |z_n|: 1.0188, 2.3381, 3.2482, 4.0879, 4.8201
zeros = [1.01879, 2.33811, 3.24820, 4.08795, 4.82010]
prefactor = (1**2 * alpha**2 / (2*1))**(1/3) # (hbar^2 alpha^2 / 2m)^(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 functions.kinetic_operator...")
# We want to verify the finite difference coefficients: 1, -2, 1 for 2nd derivative
# The code uses:
# main_diagonal = -2
# off_diagonal = 1
# Factor = -hbar^2 / (2m * dx^2)
# This corresponds to T = -hbar^2/2m * D2
# where D2 psi_i = (psi_{i+1} - 2psi_i + psi_{i-1}) / dx^2
# This is the correct 3-point central difference for the second derivative.
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__ # Reset stdout
if __name__ == "__main__":
run_verification()