Ahilan Kumaresan commited on
Commit
75d335c
·
1 Parent(s): f3feb4b

Updated the documentation for the functions

Browse files
Files changed (1) hide show
  1. functions.py +697 -53
functions.py CHANGED
@@ -21,10 +21,35 @@ global Last_k_value # Used by harmonic() and check_harmonic_analytic()
21
  # ==========================================
22
  def make_grid(L=L, N=N_GRID):
23
  """
24
- Returns:
25
- x_full: N+2 points from -L/2 to L/2 (including 'walls')
26
- dx: grid spacing
27
- x_internal: internal N points (where we solve)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  """
29
  x = np.linspace(-L/2, L/2, N+2)
30
  dx = x[1] - x[0]
@@ -34,53 +59,261 @@ def make_grid(L=L, N=N_GRID):
34
  # ==========================================
35
  # 3. POTENTIAL GENERATORS (V(x))
36
  # ==========================================
37
- def constant(x,c):
38
- """Adds a flat baseline potential."""
39
- return np.ones_like(x)*c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- def harmonic(x,k,center=0.0):
42
- """A Parabola, setting the global k-value."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  global Last_k_value
44
  Last_k_value = k
45
 
46
  constant_factor = 1
47
- potential = 0.5*k*(x - center)**2
48
  return constant_factor * potential
49
 
50
  def gaussian_well(x, center=0.0, width=1.0, depth=50):
51
- """A dip in the potential (finite well)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  return -depth * np.exp(-(x - center)**2 / (2 * width**2))
53
 
54
- def inf_sqaure_well(x,lower_bound,upper_bound):
55
- """Gives you an Infinite square well of Length L"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  HUGE_NUMBER = 1e10
57
  V = np.zeros_like(x)
58
- V[x<lower_bound] = HUGE_NUMBER
59
- V[x>upper_bound] = HUGE_NUMBER
60
  return V
61
 
62
- def inf_wall(x,side,bound):
63
- """Places an 'Infinite' wall (Penalty Method)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  V = np.zeros_like(x)
65
  HUGE_NUMBER = 9e10
66
  side = side.strip(', . ').lower()
67
 
68
- if side =='left':
69
- V[x<bound] = HUGE_NUMBER
70
- elif side =='right':
71
- V[x>bound] = HUGE_NUMBER
72
  return V
73
 
74
  def finite_barrier(x, center, width, height):
75
- """A square block in the middle."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  V = np.zeros_like(x)
77
  mask = (x > (center - width/2)) & (x < (center + width/2))
78
  V[mask] = height
79
  return V
80
 
81
- def V_double_well(x, depth=20, separation=1,center=0.0):
82
- """Quartic double well potential."""
83
- V = depth * ( (x-center)**2 - separation )**2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  return V
85
 
86
  def custom2(value,x):
@@ -90,8 +323,47 @@ def custom2(value,x):
90
  # In psi_solve2/functions.py
91
 
92
  def finite_square_well(x, lower_bound, upper_bound, depth_V):
93
- """A finite square well of a specific depth_V (height of the walls)."""
 
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  # Start with a baseline of zero potential
96
  V = np.zeros_like(x)
97
 
@@ -105,18 +377,119 @@ def finite_square_well(x, lower_bound, upper_bound, depth_V):
105
  # ==========================================
106
  # 4. SCHRÖDINGER EQUATION SOLVER
107
  # ==========================================
108
- def kinetic_operator(N, dx, hbar=hbar,m=m):
109
- """Builds the Kinetic Energy Matrix (N x N) using finite difference."""
110
- main_diagonal = (1/dx**2)*np.diag(-2*np.ones(N))
111
- off_diagonal1 = (1/dx**2)*np.diag(np.ones(N-1),-1)
112
- off_diagonal2 = (1/dx**2)*np.diag(np.ones(N-1),1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  D2 = (main_diagonal + off_diagonal1 + off_diagonal2)
114
 
115
- T = (-(hbar**2/(2*m) ) * D2 )
116
  return T
117
 
118
- def solve(T,V_full,dx):
119
- """Solves H psi = E psi (Eigenvalue problem)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  V_internal = V_full[1:-1]
121
  H = T + np.diag(V_internal)
122
 
@@ -135,7 +508,37 @@ def solve(T,V_full,dx):
135
  # 5. PLOTTING FUNCTIONS (STREAMLIT/JUPYTER SAFE)
136
  # ==========================================
137
  def plot_V(V_raw_input):
138
- """Returns a figure showing a 1D potential profile."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  if V_raw_input is None or np.ndim(V_raw_input) == 0:
140
  return None
141
 
@@ -149,13 +552,60 @@ def plot_V(V_raw_input):
149
  return fig
150
 
151
 
152
- def plot_alive(E, psi, V, x, no = 1,nos=5,mode=''):
153
  """
154
- Physically accurate plot:
155
- - |psi|^2 has its own scale on right y-axis
156
- - Potential & energy use left y-axis
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- UPDATED: Colors synchronized between probability density and energy line.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  """
160
  import matplotlib.pyplot as plt
161
 
@@ -313,17 +763,42 @@ def show_matrix(overlap_matrix,how='normal',round_value=10):
313
  plt.locator_params(axis='x', integer=True)
314
  plt.show()
315
 
316
- def check_ISW_analytic(E,L,hbar=1.0, m=1.0, max_levels=6):
317
- """Compares numerical energies to the Infinite Square Well analytic formula."""
318
- CHECK_N = max_levels
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  E_numerical = E[:CHECK_N]
320
- E_analytic = np.zeros(CHECK_N) # Changed to CHECK_N size for correct indexing
321
 
322
  for i in range(CHECK_N):
323
  n = i + 1
324
- E_analytic[i] = (hbar**2 * np.pi**2 * n**2 ) / (2*m*L**2)
325
 
326
  print("\n### ENERGY BENCHMARK: Infinite Square Well ###")
 
327
  print("-" * 55)
328
  print(f"| n | Analytic E | Numerical E | % Error |")
329
  print("-" * 55)
@@ -334,15 +809,41 @@ def check_ISW_analytic(E,L,hbar=1.0, m=1.0, max_levels=6):
334
  f"| {i+1:<1} | {E_analytic[i]:<10.6f} | {E_numerical[i]:<11.6f} | {percent_error:<7.4f}% |"
335
  )
336
  print("-" * 55)
 
 
337
 
338
- def check_harmonic_analytic(E, hbar=1.0, m=1.0, max_levels=6):
339
- """Compares numerical energies to the Harmonic Oscillator analytic formula."""
340
- CHECK_N = max_levels
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  try:
342
- k = Last_k_value
343
  if k is None:
344
- print("ERROR: k is not set. Run a harmonic potential first.")
345
- return
 
 
346
 
347
  w = np.sqrt(k/m)
348
  E_numerical = E[:CHECK_N]
@@ -350,9 +851,10 @@ def check_harmonic_analytic(E, hbar=1.0, m=1.0, max_levels=6):
350
 
351
  for i in range(CHECK_N):
352
  n_quantum = i
353
- E_analytic[i] = (n_quantum + 0.5 ) * hbar * w
354
 
355
  print("\n### ENERGY BENCHMARK: Harmonic Oscillator ###")
 
356
  print("-" * 55)
357
  print(f"| n | Analytic E | Numerical E | % Error |")
358
  print("-" * 55)
@@ -365,9 +867,115 @@ def check_harmonic_analytic(E, hbar=1.0, m=1.0, max_levels=6):
365
  f"| {n_label:<1} | {E_analytic[i]:<10.6f} | {E_numerical[i]:<11.6f} | {percent_error:<7.4f}% |"
366
  )
367
  print("-" * 55)
 
 
368
 
369
  except Exception as e:
370
- print(f"I don't think this is a Harmonic Oscillator, or an error occurred: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
 
372
 
373
 
@@ -736,3 +1344,39 @@ def capture_potential_notebook(tune, A_MIN, A_MAX, mode='wait'):
736
  # -----------------------------------------------------------------
737
  cap.release()
738
  return captured_V
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # ==========================================
22
  def make_grid(L=L, N=N_GRID):
23
  """
24
+ Create a spatial grid for solving the Schrödinger equation.
25
+
26
+ Parameters
27
+ ----------
28
+ L : float, optional
29
+ Total length of the spatial domain (default: 50 a.u.)
30
+ N : int, optional
31
+ Number of internal grid points (default: 2000)
32
+
33
+ Returns
34
+ -------
35
+ x_full : ndarray
36
+ Full grid with N+2 points from -L/2 to L/2, including boundary points
37
+ dx : float
38
+ Grid spacing (distance between adjacent points)
39
+ x_internal : ndarray
40
+ Internal grid points (N points) where the wavefunction is solved
41
+ Excludes the boundary points at x[0] and x[-1]
42
+
43
+ Notes
44
+ -----
45
+ The boundary points are used to enforce boundary conditions (typically ψ=0)
46
+ while x_internal contains the points where we actually solve for ψ.
47
+
48
+ Examples
49
+ --------
50
+ >>> x, dx, x_int = make_grid(L=20, N=1000)
51
+ >>> print(f"Domain: [{x[0]:.1f}, {x[-1]:.1f}], spacing: {dx:.4f}")
52
+ Domain: [-10.0, 10.0], spacing: 0.0200
53
  """
54
  x = np.linspace(-L/2, L/2, N+2)
55
  dx = x[1] - x[0]
 
59
  # ==========================================
60
  # 3. POTENTIAL GENERATORS (V(x))
61
  # ==========================================
62
+ def constant(x, c):
63
+ """
64
+ Create a constant potential across the entire domain.
65
+
66
+ Parameters
67
+ ----------
68
+ x : ndarray
69
+ Spatial grid points
70
+ c : float
71
+ Constant potential value (in Hartree atomic units)
72
+
73
+ Returns
74
+ -------
75
+ V : ndarray
76
+ Constant potential array of same shape as x, with value c everywhere
77
+
78
+ Examples
79
+ --------
80
+ >>> x = np.linspace(-10, 10, 100)
81
+ >>> V = constant(x, 5.0) # V(x) = 5.0 everywhere
82
+ """
83
+ return np.ones_like(x) * c
84
 
85
+ def harmonic(x, k, center=0.0):
86
+ """
87
+ Create a harmonic oscillator (parabolic) potential.
88
+
89
+ Generates V(x) = (1/2)k(x - center)² representing a quantum harmonic
90
+ oscillator potential centered at the specified position.
91
+
92
+ Parameters
93
+ ----------
94
+ x : ndarray
95
+ Spatial grid points
96
+ k : float
97
+ Spring constant (curvature parameter) in atomic units
98
+ Larger k → stiffer spring → more tightly bound states
99
+ center : float, optional
100
+ Center position of the parabola (default: 0.0)
101
+
102
+ Returns
103
+ -------
104
+ V : ndarray
105
+ Harmonic potential array: V(x) = 0.5 * k * (x - center)²
106
+
107
+ Notes
108
+ -----
109
+ - Sets global variable Last_k_value for use by check_harmonic_analytic()
110
+ - Energy levels: E_n = ℏω(n + 1/2) where ω = √(k/m)
111
+ - In atomic units (ℏ=1, m=1): ω = √k
112
+
113
+ Examples
114
+ --------
115
+ >>> x = np.linspace(-10, 10, 1000)
116
+ >>> V = harmonic(x, k=1.0, center=0.0) # Standard QHO
117
+ >>> V_stiff = harmonic(x, k=10.0, center=0.0) # Stiffer spring
118
+ >>> V_offset = harmonic(x, k=1.0, center=5.0) # Centered at x=5
119
+ """
120
  global Last_k_value
121
  Last_k_value = k
122
 
123
  constant_factor = 1
124
+ potential = 0.5 * k * (x - center)**2
125
  return constant_factor * potential
126
 
127
  def gaussian_well(x, center=0.0, width=1.0, depth=50):
128
+ """
129
+ Create a Gaussian-shaped potential well.
130
+
131
+ Generates a smooth, bell-shaped potential dip that can trap particles.
132
+
133
+ Parameters
134
+ ----------
135
+ x : ndarray
136
+ Spatial grid points
137
+ center : float, optional
138
+ Center position of the well (default: 0.0)
139
+ width : float, optional
140
+ Width parameter (standard deviation) of the Gaussian (default: 1.0)
141
+ Larger width → broader well
142
+ depth : float, optional
143
+ Depth of the well at the center (default: 50)
144
+ Positive depth creates a well (attractive potential)
145
+
146
+ Returns
147
+ -------
148
+ V : ndarray
149
+ Gaussian well potential: V(x) = -depth * exp(-(x-center)²/(2*width²))
150
+
151
+ Notes
152
+ -----
153
+ - Minimum potential is -depth at x = center
154
+ - Potential approaches 0 as |x - center| → ∞
155
+ - Smooth potential (infinitely differentiable)
156
+
157
+ Examples
158
+ --------
159
+ >>> x = np.linspace(-10, 10, 1000)
160
+ >>> V = gaussian_well(x, center=0, width=2.0, depth=10)
161
+ """
162
  return -depth * np.exp(-(x - center)**2 / (2 * width**2))
163
 
164
+ def inf_sqaure_well(x, lower_bound, upper_bound):
165
+ """
166
+ Create an infinite square well (particle in a box) potential.
167
+
168
+ Parameters
169
+ ----------
170
+ x : ndarray
171
+ Spatial grid points
172
+ lower_bound : float
173
+ Left boundary of the well
174
+ upper_bound : float
175
+ Right boundary of the well
176
+
177
+ Returns
178
+ -------
179
+ V : ndarray
180
+ Infinite square well potential:
181
+ - V(x) = 0 for lower_bound ≤ x ≤ upper_bound (inside well)
182
+ - V(x) = 10¹⁰ for x < lower_bound or x > upper_bound (outside well)
183
+
184
+ Notes
185
+ -----
186
+ - Uses penalty method: "infinite" walls are approximated by very large
187
+ potential (10¹⁰) to enforce ψ ≈ 0 outside the well
188
+ - Well width: L = upper_bound - lower_bound
189
+ - Analytical energies: E_n = (ℏ²π²n²)/(2mL²) for n = 1, 2, 3, ...
190
+
191
+ Examples
192
+ --------
193
+ >>> x = np.linspace(-15, 15, 1000)
194
+ >>> V = inf_sqaure_well(x, lower_bound=-10, upper_bound=10) # L = 20
195
+ >>> # Use with check_ISW_analytic(E, lower_bound=-10, upper_bound=10)
196
+ """
197
  HUGE_NUMBER = 1e10
198
  V = np.zeros_like(x)
199
+ V[x < lower_bound] = HUGE_NUMBER
200
+ V[x > upper_bound] = HUGE_NUMBER
201
  return V
202
 
203
+ def inf_wall(x, side, bound):
204
+ """
205
+ Place an infinite potential wall on one side of the domain.
206
+
207
+ Parameters
208
+ ----------
209
+ x : ndarray
210
+ Spatial grid points
211
+ side : str
212
+ Which side to place the wall: 'left' or 'right'
213
+ (case-insensitive, strips whitespace and punctuation)
214
+ bound : float
215
+ Position of the wall boundary
216
+
217
+ Returns
218
+ -------
219
+ V : ndarray
220
+ Potential with infinite wall:
221
+ - If side='left': V(x) = 10¹⁰ for x < bound, V(x) = 0 for x ≥ bound
222
+ - If side='right': V(x) = 10¹⁰ for x > bound, V(x) = 0 for x ≤ bound
223
+
224
+ Notes
225
+ -----
226
+ Uses penalty method with V = 9×10¹⁰ to approximate infinite potential.
227
+
228
+ Examples
229
+ --------
230
+ >>> x = np.linspace(-10, 10, 1000)
231
+ >>> V_left = inf_wall(x, 'left', bound=-5) # Wall at x=-5, blocks left side
232
+ >>> V_right = inf_wall(x, 'right', bound=5) # Wall at x=5, blocks right side
233
+ """
234
  V = np.zeros_like(x)
235
  HUGE_NUMBER = 9e10
236
  side = side.strip(', . ').lower()
237
 
238
+ if side == 'left':
239
+ V[x < bound] = HUGE_NUMBER
240
+ elif side == 'right':
241
+ V[x > bound] = HUGE_NUMBER
242
  return V
243
 
244
  def finite_barrier(x, center, width, height):
245
+ """
246
+ Create a finite rectangular potential barrier.
247
+
248
+ Parameters
249
+ ----------
250
+ x : ndarray
251
+ Spatial grid points
252
+ center : float
253
+ Center position of the barrier
254
+ width : float
255
+ Total width of the barrier
256
+ height : float
257
+ Height of the potential barrier
258
+
259
+ Returns
260
+ -------
261
+ V : ndarray
262
+ Rectangular barrier potential:
263
+ - V(x) = height for |x - center| < width/2
264
+ - V(x) = 0 elsewhere
265
+
266
+ Notes
267
+ -----
268
+ Useful for studying quantum tunneling phenomena. Particles with E < height
269
+ can tunnel through the barrier with exponentially decaying probability.
270
+
271
+ Examples
272
+ --------
273
+ >>> x = np.linspace(-10, 10, 1000)
274
+ >>> V = finite_barrier(x, center=0, width=2, height=5) # Barrier from x=-1 to x=1
275
+ """
276
  V = np.zeros_like(x)
277
  mask = (x > (center - width/2)) & (x < (center + width/2))
278
  V[mask] = height
279
  return V
280
 
281
+ def V_double_well(x, depth=20, separation=1, center=0.0):
282
+ """
283
+ Create a quartic double-well potential.
284
+
285
+ Generates V(x) = depth × ((x-center)² - separation)² which has two minima
286
+ separated by a central barrier.
287
+
288
+ Parameters
289
+ ----------
290
+ x : ndarray
291
+ Spatial grid points
292
+ depth : float, optional
293
+ Depth parameter controlling overall potential strength (default: 20)
294
+ separation : float, optional
295
+ Controls the distance between the two wells (default: 1)
296
+ Well minima are approximately at x = center ± separation
297
+ center : float, optional
298
+ Center position of the double well system (default: 0.0)
299
+
300
+ Returns
301
+ -------
302
+ V : ndarray
303
+ Double well potential: V(x) = depth × ((x-center)² - separation)²
304
+
305
+ Notes
306
+ -----
307
+ - Creates symmetric double well with barrier at x = center
308
+ - Useful for studying tunneling splitting and symmetric/antisymmetric states
309
+ - Ground state and first excited state form tunneling doublet
310
+
311
+ Examples
312
+ --------
313
+ >>> x = np.linspace(-5, 5, 1000)
314
+ >>> V = V_double_well(x, depth=2, separation=1, center=0)
315
+ """
316
+ V = depth * ((x - center)**2 - separation)**2
317
  return V
318
 
319
  def custom2(value,x):
 
323
  # In psi_solve2/functions.py
324
 
325
  def finite_square_well(x, lower_bound, upper_bound, depth_V):
326
+ """
327
+ Create a finite square well potential.
328
 
329
+ The potential is zero inside the well and has finite height depth_V outside.
330
+ Unlike the infinite square well, particles can exist in the barrier region
331
+ with exponentially decaying wavefunctions.
332
+
333
+ Parameters
334
+ ----------
335
+ x : ndarray
336
+ Spatial grid points
337
+ lower_bound : float
338
+ Left boundary of the well
339
+ upper_bound : float
340
+ Right boundary of the well
341
+ depth_V : float
342
+ Height of the potential barriers outside the well (V₀)
343
+
344
+ Returns
345
+ -------
346
+ V : ndarray
347
+ Finite square well potential:
348
+ - V(x) = 0 for lower_bound ≤ x ≤ upper_bound (inside well)
349
+ - V(x) = depth_V for x < lower_bound or x > upper_bound (barrier regions)
350
+
351
+ """
352
+ """
353
+ Notes
354
+ -----
355
+ - Bound states exist only when E < depth_V
356
+ - Number of bound states depends on well width and depth_V
357
+ - For bound states, wavefunction decays exponentially in barrier (E < V)
358
+ - For scattering states (E > depth_V), wavefunction oscillates everywhere
359
+ - Use check_finite_well_analytic() to verify numerical results
360
+
361
+ Examples
362
+ --------
363
+ >>> x = np.linspace(-15, 15, 1000)
364
+ >>> V_deep = finite_square_well(x, -10, 10, depth_V=2.0) # Deep well, many bound states
365
+ >>> V_shallow = finite_square_well(x, -10, 10, depth_V=0.01) # Shallow, few/no bound states
366
+ """
367
  # Start with a baseline of zero potential
368
  V = np.zeros_like(x)
369
 
 
377
  # ==========================================
378
  # 4. SCHRÖDINGER EQUATION SOLVER
379
  # ==========================================
380
+ def kinetic_operator(N, dx, hbar=hbar, m=m):
381
+ """
382
+ Build the kinetic energy operator matrix using finite difference method.
383
+
384
+ Constructs the discrete representation of the kinetic energy operator
385
+ T = -(ℏ²/2m) d²/dx² using a 3-point central difference stencil.
386
+
387
+ Parameters
388
+ ----------
389
+ N : int
390
+ Number of internal grid points (size of the matrix)
391
+ dx : float
392
+ Grid spacing (distance between adjacent points)
393
+ hbar : float, optional
394
+ Reduced Planck constant (default: 1.0 in atomic units)
395
+ m : float, optional
396
+ Particle mass (default: 1.0 in atomic units)
397
+
398
+ Returns
399
+ -------
400
+ T : ndarray, shape (N, N)
401
+ Kinetic energy operator matrix (symmetric, tridiagonal)
402
+ - Diagonal elements: -(ℏ²/2m) × (-2/dx²)
403
+ - Off-diagonal elements: -(ℏ²/2m) × (1/dx²)
404
+
405
+ """
406
+ """
407
+ Notes
408
+ -----
409
+ The second derivative is approximated using central differences:
410
+ d²ψ/dx² ≈ (ψ_{i+1} - 2ψ_i + ψ_{i-1}) / dx²
411
+
412
+ This creates a tridiagonal matrix:
413
+ - Main diagonal: -2/dx²
414
+ - Upper/lower diagonals: +1/dx²
415
+
416
+ The kinetic energy operator is then: T = -(ℏ²/2m) × D2
417
+
418
+ Examples
419
+ --------
420
+ >>> N = 1000
421
+ >>> dx = 0.025
422
+ >>> T = kinetic_operator(N, dx)
423
+ >>> print(f"Matrix shape: {T.shape}, Symmetric: {np.allclose(T, T.T)}")
424
+ Matrix shape: (1000, 1000), Symmetric: True
425
+ """
426
+ main_diagonal = (1/dx**2) * np.diag(-2 * np.ones(N))
427
+ off_diagonal1 = (1/dx**2) * np.diag(np.ones(N-1), -1)
428
+ off_diagonal2 = (1/dx**2) * np.diag(np.ones(N-1), 1)
429
  D2 = (main_diagonal + off_diagonal1 + off_diagonal2)
430
 
431
+ T = (-(hbar**2 / (2*m)) * D2)
432
  return T
433
 
434
+ def solve(T, V_full, dx):
435
+ """
436
+ Solve the time-independent Schrödinger equation for eigenvalues and eigenvectors.
437
+
438
+ Solves the eigenvalue problem Hψ = Eψ where H = T + V is the Hamiltonian.
439
+ Returns normalized eigenstates sorted by energy.
440
+
441
+ Parameters
442
+ ----------
443
+ T : ndarray, shape (N, N)
444
+ Kinetic energy operator matrix from kinetic_operator()
445
+ V_full : ndarray, shape (N+2,)
446
+ Full potential array including boundary points
447
+ V_full[0] and V_full[-1] are boundary values (typically very large)
448
+ V_full[1:-1] are the internal potential values
449
+ dx : float
450
+ Grid spacing used for normalization
451
+
452
+ Returns
453
+ -------
454
+ E : ndarray, shape (N,)
455
+ Eigenvalues (energy levels) sorted in ascending order
456
+ Units: Hartree (atomic units)
457
+ psi : ndarray, shape (N, N)
458
+ Eigenvectors (wavefunctions) as columns
459
+ psi[:, i] is the wavefunction for energy E[i]
460
+ Each wavefunction is normalized: ∫|ψ|² dx = 1
461
+
462
+ """
463
+ """
464
+ Notes
465
+ -----
466
+ - Uses np.linalg.eigh() which assumes Hermitian matrix (guaranteed for H)
467
+ - Automatically sorts eigenvalues and eigenvectors by energy
468
+ - Normalizes each eigenstate using trapezoidal rule: ∫|ψ|² dx = 1
469
+ - Boundary conditions are enforced by V_full having large values at edges
470
+
471
+ The Hamiltonian is constructed as:
472
+ H = T + diag(V_internal)
473
+ where V_internal = V_full[1:-1]
474
+
475
+ Examples
476
+ --------
477
+ >>> # Setup
478
+ >>> x, dx, x_int = make_grid(L=20, N=1000)
479
+ >>> T = kinetic_operator(len(x_int), dx)
480
+ >>>
481
+ >>> # Create infinite square well
482
+ >>> V = inf_sqaure_well(x_int, -10, 10)
483
+ >>> V_full = np.pad(V, (1,1), constant_values=1e10)
484
+ >>>
485
+ >>> # Solve
486
+ >>> E, psi = solve(T, V_full, dx)
487
+ >>> print(f"Ground state energy: {E[0]:.6f} Ha")
488
+ >>>
489
+ >>> # Verify normalization
490
+ >>> norm = np.sum(psi[:, 0]**2) * dx
491
+ >>> print(f"Normalization: {norm:.6f}") # Should be 1.0
492
+ """
493
  V_internal = V_full[1:-1]
494
  H = T + np.diag(V_internal)
495
 
 
508
  # 5. PLOTTING FUNCTIONS (STREAMLIT/JUPYTER SAFE)
509
  # ==========================================
510
  def plot_V(V_raw_input):
511
+ """
512
+ Plot a 1D potential profile.
513
+
514
+ Creates a simple matplotlib figure showing the potential energy landscape.
515
+
516
+ Parameters
517
+ ----------
518
+ V_raw_input : ndarray or None
519
+ 1D array representing the potential V(x)
520
+ If None or scalar, returns None
521
+
522
+ Returns
523
+ -------
524
+ fig : matplotlib.figure.Figure or None
525
+ Figure object containing the potential plot
526
+ Returns None if input is invalid
527
+ """
528
+ """
529
+ Notes
530
+ -----
531
+ - Uses dark background style
532
+ - Cyan color for potential curve
533
+ - Useful for quick visualization of potential shapes
534
+
535
+ Examples
536
+ --------
537
+ >>> x = np.linspace(-10, 10, 1000)
538
+ >>> V = harmonic(x, k=1.0)
539
+ >>> fig = plot_V(V)
540
+ >>> plt.show()
541
+ """
542
  if V_raw_input is None or np.ndim(V_raw_input) == 0:
543
  return None
544
 
 
552
  return fig
553
 
554
 
555
+ def plot_alive(E, psi, V, x, no=1, nos=5, mode=''):
556
  """
557
+ Plot wavefunctions as probability densities with separate energy and probability axes.
558
+
559
+ Creates a physically accurate plot showing:
560
+ - Potential V(x) and energy levels on left y-axis
561
+ - Probability densities |ψ|² on right y-axis (separate scale)
562
+ - Color-synchronized between probability curves and energy levels
563
+
564
+ Parameters
565
+ ----------
566
+ E : ndarray
567
+ Energy eigenvalues (in Hartree)
568
+ psi : ndarray, shape (N, M)
569
+ Wavefunction array where psi[:, i] is the i-th eigenstate
570
+ V : ndarray, shape (N+2,)
571
+ Full potential array including boundaries
572
+ x : ndarray, shape (N+2,)
573
+ Full spatial grid including boundaries
574
+ no : int, optional
575
+ State index to plot if mode != 'all' (default: 1)
576
+ nos : int, optional
577
+ Number of states to plot if mode == 'all' (default: 5)
578
+ mode : str, optional
579
+ Plot mode:
580
+ - 'all': Plot multiple states (first nos states)
581
+ - '': Plot single state (state no)
582
+ Default: '' (single state)
583
+
584
+ Returns
585
+ -------
586
+ fig : matplotlib.figure.Figure
587
+ Figure object with dual y-axes
588
+ - ax1 (left): Energy/Potential scale
589
+ - ax2 (right): Probability density scale
590
 
591
+ Notes
592
+ -----
593
+ - Uses dark background theme
594
+ - Probability densities are plotted as |ψ|², not ψ
595
+ - Each state has matching colors for its probability curve and energy level
596
+ - Regions where V > 10⁵ are hidden (infinite walls)
597
+
598
+ """
599
+ """
600
+ Examples
601
+ --------
602
+ >>> # Plot first 5 states
603
+ >>> fig = plot_alive(E, psi, V_full, x_full, nos=5, mode='all')
604
+ >>> plt.show()
605
+ >>>
606
+ >>> # Plot only ground state
607
+ >>> fig = plot_alive(E, psi, V_full, x_full, no=0)
608
+ >>> plt.show()
609
  """
610
  import matplotlib.pyplot as plt
611
 
 
763
  plt.locator_params(axis='x', integer=True)
764
  plt.show()
765
 
766
+ def check_ISW_analytic(E, lower_bound=-10, upper_bound=10, hbar=1.0, m=1.0, max_levels=6):
767
+ """
768
+ Compares numerical energies to the Infinite Square Well analytic formula.
769
+
770
+ Parameters:
771
+ -----------
772
+ E : array
773
+ Numerical eigenvalues
774
+ lower_bound : float
775
+ Lower boundary of the well (default: -10)
776
+ upper_bound : float
777
+ Upper boundary of the well (default: 10)
778
+ hbar : float
779
+ Reduced Planck constant (default: 1.0)
780
+ m : float
781
+ Particle mass (default: 1.0)
782
+ max_levels : int
783
+ Number of levels to check (default: 6)
784
+
785
+ """
786
+ """
787
+ Example:
788
+ --------
789
+ check_ISW_analytic(E, lower_bound=-10, upper_bound=10)
790
+ """
791
+ L = upper_bound - lower_bound # Well width
792
+ CHECK_N = min(max_levels, len(E))
793
  E_numerical = E[:CHECK_N]
794
+ E_analytic = np.zeros(CHECK_N)
795
 
796
  for i in range(CHECK_N):
797
  n = i + 1
798
+ E_analytic[i] = (hbar**2 * np.pi**2 * n**2) / (2*m*L**2)
799
 
800
  print("\n### ENERGY BENCHMARK: Infinite Square Well ###")
801
+ print(f"Well boundaries: x = [{lower_bound}, {upper_bound}], Width L = {L}")
802
  print("-" * 55)
803
  print(f"| n | Analytic E | Numerical E | % Error |")
804
  print("-" * 55)
 
809
  f"| {i+1:<1} | {E_analytic[i]:<10.6f} | {E_numerical[i]:<11.6f} | {percent_error:<7.4f}% |"
810
  )
811
  print("-" * 55)
812
+
813
+ return E_analytic, E_numerical
814
 
815
+ def check_harmonic_analytic(E, k=None, center=0.0, hbar=1.0, m=1.0, max_levels=6):
816
+ """
817
+ Compares numerical energies to the Harmonic Oscillator analytic formula.
818
+
819
+ Parameters:
820
+ -----------
821
+ E : array
822
+ Numerical eigenvalues
823
+ k : float, optional
824
+ Spring constant. If None, uses Last_k_value global variable
825
+ center : float
826
+ Center position of the harmonic oscillator (default: 0.0)
827
+ hbar : float
828
+ Reduced Planck constant (default: 1.0)
829
+ m : float
830
+ Particle mass (default: 1.0)
831
+ max_levels : int
832
+ Number of levels to check (default: 6)
833
+
834
+ Example:
835
+ --------
836
+ check_harmonic_analytic(E, k=10, center=0)
837
+ """
838
+ CHECK_N = min(max_levels, len(E))
839
+
840
  try:
841
+ # Use provided k or fall back to global Last_k_value
842
  if k is None:
843
+ k = Last_k_value
844
+ if k is None:
845
+ print("ERROR: k is not set. Please provide k parameter or run harmonic() first.")
846
+ return
847
 
848
  w = np.sqrt(k/m)
849
  E_numerical = E[:CHECK_N]
 
851
 
852
  for i in range(CHECK_N):
853
  n_quantum = i
854
+ E_analytic[i] = (n_quantum + 0.5) * hbar * w
855
 
856
  print("\n### ENERGY BENCHMARK: Harmonic Oscillator ###")
857
+ print(f"Spring constant k = {k}, Center = {center}, omega = {w:.4f}")
858
  print("-" * 55)
859
  print(f"| n | Analytic E | Numerical E | % Error |")
860
  print("-" * 55)
 
867
  f"| {n_label:<1} | {E_analytic[i]:<10.6f} | {E_numerical[i]:<11.6f} | {percent_error:<7.4f}% |"
868
  )
869
  print("-" * 55)
870
+
871
+ return E_analytic, E_numerical
872
 
873
  except Exception as e:
874
+ print(f"Error in harmonic oscillator check: {e}")
875
+
876
+
877
+ def check_finite_well_analytic(E, V0, lower_bound=-10, upper_bound=10, hbar=1.0, m=1.0, max_levels=10):
878
+ """
879
+ Compares numerical energies to the Finite Square Well analytical solution.
880
+
881
+ The finite square well has no simple closed-form solution, but bound state
882
+ energies can be found by solving transcendental equations numerically.
883
+
884
+ Parameters:
885
+ -----------
886
+ E : array
887
+ Numerical eigenvalues from your solver
888
+ V0 : float
889
+ Barrier height (potential outside the well)
890
+ lower_bound : float
891
+ Lower boundary of the well (default: -10)
892
+ upper_bound : float
893
+ Upper boundary of the well (default: 10)
894
+ hbar : float
895
+ Reduced Planck constant (default: 1.0)
896
+ m : float
897
+ Particle mass (default: 1.0)
898
+ max_levels : int
899
+ Maximum number of levels to check (default: 10)
900
+
901
+ Example:
902
+ --------
903
+ check_finite_well_analytic(E, V0=2.0, lower_bound=-10, upper_bound=10)
904
+ """
905
+ a = (upper_bound - lower_bound) / 2 # Half-width
906
+ z0 = a * np.sqrt(2 * m * V0) / hbar # Dimensionless parameter
907
+
908
+ # Find analytical energies by solving transcendental equations
909
+ E_analytic = []
910
+
911
+ # Even parity states: z*tan(z) = sqrt(z0^2 - z^2)
912
+ z_vals = np.linspace(0.01, z0 - 0.01, 10000)
913
+ for n in range(max_levels):
914
+ try:
915
+ lhs = z_vals * np.tan(z_vals)
916
+ rhs = np.sqrt(z0**2 - z_vals**2)
917
+ diff = lhs - rhs
918
+
919
+ # Find sign changes (crossings)
920
+ for i in range(len(diff) - 1):
921
+ if diff[i] * diff[i+1] < 0:
922
+ z = z_vals[i]
923
+ E_candidate = (hbar**2 * z**2) / (2 * m * a**2)
924
+ if E_candidate < V0 and not any(np.isclose(E_candidate, E_a, rtol=1e-3) for E_a in E_analytic):
925
+ E_analytic.append(E_candidate)
926
+ break
927
+ except:
928
+ pass
929
+
930
+ # Odd parity states: -z*cot(z) = sqrt(z0^2 - z^2)
931
+ for n in range(max_levels):
932
+ try:
933
+ lhs = -z_vals / np.tan(z_vals)
934
+ rhs = np.sqrt(z0**2 - z_vals**2)
935
+ diff = lhs - rhs
936
+
937
+ for i in range(len(diff) - 1):
938
+ if diff[i] * diff[i+1] < 0:
939
+ z = z_vals[i]
940
+ E_candidate = (hbar**2 * z**2) / (2 * m * a**2)
941
+ if E_candidate < V0 and not any(np.isclose(E_candidate, E_a, rtol=1e-3) for E_a in E_analytic):
942
+ E_analytic.append(E_candidate)
943
+ break
944
+ except:
945
+ pass
946
+
947
+ E_analytic = sorted(E_analytic)
948
+
949
+ # Filter numerical energies to only bound states
950
+ E_numerical_bound = E[E < V0]
951
+
952
+ CHECK_N = min(len(E_analytic), len(E_numerical_bound), max_levels)
953
+
954
+ if CHECK_N == 0:
955
+ print("\n### ENERGY BENCHMARK: Finite Square Well ###")
956
+ print(f"Well: x in [{lower_bound}, {upper_bound}], V0 = {V0}, z0 = {z0:.4f}")
957
+ print("WARNING: No bound states found!")
958
+ print(f" Barrier too shallow. Need V0 > {E[0]:.4f} to bind the ground state.")
959
+ return None, None
960
+
961
+ print("\n### ENERGY BENCHMARK: Finite Square Well ###")
962
+ print(f"Well: x in [{lower_bound}, {upper_bound}], V0 = {V0}, z0 = {z0:.4f}")
963
+ print(f"Number of bound states: {CHECK_N}")
964
+ print("-" * 55)
965
+ print(f"| n | Analytic E | Numerical E | % Error |")
966
+ print("-" * 55)
967
+
968
+ for i in range(CHECK_N):
969
+ percent_error = np.abs((E_numerical_bound[i] - E_analytic[i]) / E_analytic[i]) * 100
970
+ print(
971
+ f"| {i:<1} | {E_analytic[i]:<10.6f} | {E_numerical_bound[i]:<11.6f} | {percent_error:<7.4f}% |"
972
+ )
973
+ print("-" * 55)
974
+
975
+ return np.array(E_analytic[:CHECK_N]), E_numerical_bound[:CHECK_N]
976
+
977
+
978
+
979
 
980
 
981
 
 
1344
  # -----------------------------------------------------------------
1345
  cap.release()
1346
  return captured_V
1347
+
1348
+
1349
+
1350
+
1351
+
1352
+ ###
1353
+ import qrcode
1354
+ from IPython.display import display, Image
1355
+
1356
+ def show_QR(url):
1357
+ # The file name to save the QR code image
1358
+ file_name = "hand_wave_link_qrcode.png"
1359
+
1360
+ # --- QR Code Generation ---
1361
+ # 1. Create a QR code object with specific settings
1362
+ qr = qrcode.QRCode(
1363
+ version=1,
1364
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
1365
+ box_size=10,
1366
+ border=4,
1367
+ )
1368
+
1369
+ # 2. Add the URL data to the object
1370
+ qr.add_data(url)
1371
+ qr.make(fit=True)
1372
+
1373
+ # 3. Create the QR code image
1374
+ img = qr.make_image(fill_color="black", back_color="white")
1375
+
1376
+ # 4. Save the image to the local directory
1377
+ img.save(file_name)
1378
+
1379
+ # --- Display in Jupyter Notebook ---
1380
+
1381
+ # 5. Display the saved image using IPython.display
1382
+ return display(Image(filename=file_name))