AhiBucket commited on
Commit
2d541d6
·
verified ·
1 Parent(s): fcd543a

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +533 -529
app.py CHANGED
@@ -1,529 +1,533 @@
1
- import streamlit as st
2
-
3
- # ==========================================
4
- # 0. PAGE CONFIGURATION & STYLING
5
- # ==========================================
6
- st.set_page_config(
7
- page_title="Pili-Pili Quantum Solver | Ahilan Kumaresan",
8
- page_icon="🍟",
9
- layout="wide",
10
- initial_sidebar_state="expanded"
11
- )
12
-
13
- import numpy as np
14
- import matplotlib.pyplot as plt
15
- import math
16
- import time
17
-
18
- try:
19
- import mediapipe as mp
20
- import cv2
21
- import plotly.graph_objects as go
22
- from plotly.subplots import make_subplots
23
- except ImportError as e:
24
- st.error(f"CRITICAL ERROR: Failed to import required libraries. {e}")
25
- st.stop()
26
-
27
- # Import physics engine
28
- try:
29
- import functions as f
30
- except ImportError as e:
31
- st.error(f"CRITICAL ERROR: Failed to import physics engine. {e}")
32
- st.stop()
33
-
34
-
35
- # ==========================================
36
- # 0. SESSION STATE (for camera flow)
37
- # ==========================================
38
- if 'countdown_finished' not in st.session_state:
39
- st.session_state.countdown_finished = False
40
- if 'V_user_defined' not in st.session_state:
41
- st.session_state.V_user_defined = None
42
-
43
- # Custom CSS for a professional look
44
- st.markdown("""
45
- <style>
46
- .main {
47
- background-color: #0e1117;
48
- }
49
- .stButton>button {
50
- width: 100%;
51
- border-radius: 5px;
52
- height: 3em;
53
- background-color: #262730;
54
- color: white;
55
- border: 1px solid #4b4b4b;
56
- }
57
- .stButton>button:hover {
58
- border-color: #00ADB5;
59
- color: #00ADB5;
60
- }
61
- h1, h2, h3 {
62
- color: #00ADB5;
63
- font-family: 'Helvetica Neue', sans-serif;
64
- }
65
- </style>
66
- """, unsafe_allow_html=True)
67
-
68
- # ==========================================
69
- # 1. SIDEBAR: PERSONALIZATION & NAV
70
- # ==========================================
71
- with st.sidebar:
72
- st.title("Quantum Solver 2.0")
73
- st.markdown("---")
74
-
75
- # Navigation
76
- page = st.radio("Navigation", ["Simulator", "Benchmarks & Verification", "Theory & Method"])
77
-
78
- st.markdown("---")
79
-
80
- # Author Profile
81
- st.markdown("### About Moi")
82
- st.markdown("""
83
- **Ahilan Kumaresan**
84
-
85
- *Aspiring Mathematical & Computational Physicist*
86
-
87
- Developing Interative and accurate numerical tools for quantum mechanics.
88
- """)
89
-
90
- st.info("Verified against Analytical Solutions & QMSolve Package.")
91
-
92
- # ==========================================
93
- # 2. HELPER FUNCTIONS (Plotting)
94
- # ==========================================
95
- def plot_interactive(E, psi, V, x, nos=5):
96
- """
97
- Creates a professional interactive Plotly chart for wavefunctions and energy levels.
98
- """
99
- # Limit states
100
- states = min(nos, len(E))
101
-
102
- # Create subplots: Main plot (Potential + Psi) and Side plot (Energy Levels)
103
- fig = make_subplots(
104
- rows=1, cols=2,
105
- column_widths=[0.8, 0.2],
106
- shared_yaxes=True,
107
- horizontal_spacing=0.02,
108
- subplot_titles=("Wavefunctions & Potential", "Energy Spectrum")
109
- )
110
-
111
- # Scaling factor for wavefunctions
112
- if len(E) >= 2:
113
- scale = (E[1] - E[0]) * 0.4
114
- else:
115
- scale = max(E[0] * 0.1, 0.5)
116
-
117
- max_E = E[states-1] if states > 0 else 10
118
- window_height = max_E * 1.5
119
-
120
- # Get x coordinates for internal points (matching psi dimensions)
121
- x_internal = x[1:-1]
122
- V_internal = V[1:-1]
123
-
124
- # 1. Plot Potential V(x) - using internal points for better visibility
125
- V_clipped = np.clip(V_internal, 0, window_height)
126
-
127
- fig.add_trace(
128
- go.Scatter(
129
- x=x_internal.tolist() if hasattr(x_internal, 'tolist') else x_internal,
130
- y=V_clipped.tolist() if hasattr(V_clipped, 'tolist') else V_clipped,
131
- mode='lines',
132
- name='V(x)',
133
- line=dict(color='#FFFFFF', width=2.5),
134
- hovertemplate='V(x): %{y:.2f}<extra></extra>'
135
- ),
136
- row=1, col=1
137
- )
138
-
139
- # 2. Plot Wavefunctions (shifted by Energy)
140
- colors = ['#00ADB5', '#FF2E63', '#F38181', '#FCE38A', '#EAFFD0',
141
- '#95E1D3', '#FFB6C1', '#DDA0DD', '#87CEEB', '#98FB98']
142
-
143
- for n in range(states):
144
- # Normalize wavefunction amplitude
145
- psi_n = psi[:, n]
146
- max_amp = np.max(np.abs(psi_n))
147
- if max_amp > 1e-9:
148
- psi_n = psi_n / max_amp
149
- else:
150
- psi_n = psi_n
151
-
152
- # Shift by energy
153
- y_shifted = psi_n * scale + E[n]
154
-
155
- # Hide where potential is infinite
156
- y_shifted[V_internal > 1e5] = np.nan
157
-
158
- color = colors[n % len(colors)]
159
-
160
- # Ensure arrays match in length
161
- if len(x_internal) != len(y_shifted):
162
- # Fallback: truncate to minimum length
163
- min_len = min(len(x_internal), len(y_shifted))
164
- x_plot = x_internal[:min_len]
165
- y_plot = y_shifted[:min_len]
166
- else:
167
- x_plot = x_internal
168
- y_plot = y_shifted
169
-
170
- fig.add_trace(
171
- go.Scatter(
172
- x=x_plot.tolist() if hasattr(x_plot, 'tolist') else x_plot,
173
- y=y_plot.tolist() if hasattr(y_plot, 'tolist') else y_plot,
174
- mode='lines',
175
- name=f'n={n+1}, E={E[n]:.4f}',
176
- line=dict(color=color, width=2),
177
- hovertemplate=f'n={n+1}<br>E={E[n]:.4f}<br>x: %{{x:.2f}}<br>ψ: %{{y:.2f}}<extra></extra>'
178
- ),
179
- row=1, col=1
180
- )
181
-
182
- # Add Energy Level to Side Bar
183
- fig.add_trace(
184
- go.Scatter(
185
- x=[0, 1], y=[E[n], E[n]],
186
- mode='lines',
187
- line=dict(color=color, width=3),
188
- showlegend=False,
189
- hovertemplate=f'E_{n+1}={E[n]:.4f}<extra></extra>'
190
- ),
191
- row=1, col=2
192
- )
193
-
194
- # Layout Styling - Enhanced dark mode
195
- fig.update_layout(
196
- template="plotly_dark",
197
- height=600,
198
- margin=dict(l=20, r=20, t=50, b=20),
199
- legend=dict(
200
- orientation="h",
201
- yanchor="bottom",
202
- y=1.02,
203
- xanchor="right",
204
- x=1,
205
- font=dict(size=10)
206
- ),
207
- hovermode="closest",
208
- plot_bgcolor='#0e1117',
209
- paper_bgcolor='#0e1117',
210
- font=dict(color='#FAFAFA')
211
- )
212
-
213
- fig.update_xaxes(
214
- title_text="Position (a.u.)",
215
- row=1, col=1,
216
- gridcolor='#2a2a2a',
217
- showgrid=True
218
- )
219
- fig.update_xaxes(
220
- showticklabels=False,
221
- row=1, col=2,
222
- showgrid=False
223
- )
224
- fig.update_yaxes(
225
- title_text="Energy (Hartree)",
226
- range=[0, max_E * 1.2],
227
- row=1, col=1,
228
- gridcolor='#2a2a2a',
229
- showgrid=True
230
- )
231
-
232
- return fig
233
-
234
- # ==========================================
235
- # 3. HELPER: MediaPipe hand → 1D potential
236
- # ==========================================
237
- def process_frame_to_potential(frame):
238
- """
239
- Takes a BGR frame (OpenCV) and returns:
240
- pot_profile: 1D array in [0,1] representing V(x) profile
241
- msg: human-friendly label
242
- Modes:
243
- - 2 hands → Square well (0 inside, 1 outside)
244
- - 1 handQHO-like parabola
245
- """
246
- mp_hands = mp.solutions.hands
247
-
248
- with mp_hands.Hands(max_num_hands=2, min_detection_confidence=0.5) as hands:
249
- h, w, _ = frame.shape
250
- rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
251
- res = hands.process(rgb)
252
-
253
- if not res.multi_hand_landmarks:
254
- return None, "No Hands Detected, But Cute Smile :)"
255
-
256
- # --- LOGIC: Square Well vs QHO ---
257
-
258
- # 1. Square Well (2 Hands)
259
- if len(res.multi_hand_landmarks) >= 2:
260
- INDEX_TIP_ID = 8
261
- x_coords = [lm.landmark[INDEX_TIP_ID].x * w for lm in res.multi_hand_landmarks]
262
- x_coords.sort()
263
-
264
- xL_hand, xR_hand = x_coords[0], x_coords[1]
265
- well_width = xR_hand - xL_hand
266
-
267
- center_screen = w / 2
268
- centered_L = center_screen - (well_width / 2)
269
- centered_R = center_screen + (well_width / 2)
270
-
271
- x_space = np.linspace(0, w, 400)
272
- pot_profile = np.ones_like(x_space)
273
- pot_profile[(x_space > centered_L) & (x_space < centered_R)] = 0
274
-
275
- return pot_profile, "Square Well (Captured)"
276
-
277
- # 2. Harmonic Oscillator (1 Hand)
278
- elif len(res.multi_hand_landmarks) == 1:
279
- lm = res.multi_hand_landmarks[0]
280
- THUMB = lm.landmark[4]
281
- INDEX = lm.landmark[8]
282
-
283
- dx = INDEX.x - THUMB.x
284
- dy = INDEX.y - THUMB.y
285
- dist = math.sqrt(dx**2 + dy**2)
286
-
287
- # Map pinch distance → curvature
288
- A = np.interp(dist, [0.05, 0.3], [100.0, 1.0])
289
-
290
- x_space = np.linspace(-1, 1, 400)
291
- pot_profile = A * (x_space**2)
292
-
293
- pot_profile = np.clip(pot_profile, 0, 100)
294
- pot_profile = pot_profile / 100.0 # normalize 0..1
295
-
296
- return pot_profile, f"Harmonic Oscillator (k={A:.1f})"
297
-
298
- return None, "Error"
299
-
300
- # ==========================================
301
- # 4. PAGE: SIMULATOR
302
- # ==========================================
303
- if page == "Simulator":
304
- st.title("Pili-Pili - Quantum Potential Solver")
305
- st.markdown("Show a potential with your hands or select a preset to solve the **Time-Independent Schrödinger Equation**.")
306
-
307
- # Shared grid for all modes
308
- L = 50
309
- N_GRID = 1000
310
- x_full, dx, x_internal = f.make_grid(L, N_GRID)
311
-
312
- V_full_to_solve = None
313
- status_msg = ""
314
-
315
- col1, col2 = st.columns([1, 3])
316
-
317
- with col1:
318
- st.subheader("Controls")
319
-
320
- # Settings
321
- potential_mode = st.selectbox(
322
- "Potential Type",
323
- [
324
- "Static Square Well",
325
- "Static Harmonic Oscillator",
326
- "Double Well",
327
- "Hand Gesture (Camera)"
328
- ]
329
- )
330
-
331
- nos_user = st.slider("Eigenstates to Plot", 1, 10, 5)
332
-
333
- # ---- STATIC MODES ----
334
- if potential_mode == "Static Square Well":
335
- width = st.slider("Well Width", 1.0, 20.0, 10.0)
336
- V_physics = np.zeros_like(x_internal)
337
- V_physics[np.abs(x_internal) > width/2] = 200
338
- V_full_to_solve = np.pad(V_physics, (1,1), constant_values=1e10)
339
- status_msg = f"Static Square Well (width = {width:.1f})"
340
-
341
- elif potential_mode == "Static Harmonic Oscillator":
342
- k = st.slider("Spring Constant (k)", 0.1, 50.0, 5.0)
343
- V_physics = 0.5 * k * x_internal**2
344
- # scale a bit so it shows nicely under energies
345
- V_physics = V_physics / np.max(V_physics) * 50
346
- V_full_to_solve = np.pad(V_physics, (1,1), constant_values=1e10)
347
- status_msg = f"Static Harmonic Oscillator (k = {k:.2f})"
348
-
349
- elif potential_mode == "Double Well":
350
- sep = st.slider("Separation", 0.5, 5.0, 2.0)
351
- depth = st.slider("Depth", 0.1, 5.0, 1.0)
352
- V_physics = depth * ((x_internal**2 - sep**2)**2)
353
- V_physics = V_physics / np.max(V_physics) * 50
354
- V_full_to_solve = np.pad(V_physics, (1,1), constant_values=1e10)
355
- status_msg = f"Double Well (sep = {sep:.2f}, depth = {depth:.2f})"
356
-
357
- # ---- HAND-GESTURE / CAMERA MODE ----
358
- elif potential_mode == "Hand Gesture (Camera)":
359
- st.subheader("Hand Gesture Controls")
360
- st.info(
361
- "1. Click **'Start Countdown'**. (IGNORE)\n"
362
- "2. Get your **two hands** ready for a Square Well, "
363
- "or **one-hand pinch** for a Harmonic Oscillator.\n"
364
- "3. When you'r ready, use **'Take a snapshot'**."
365
- )
366
-
367
-
368
- st.subheader("Hand Gesture Input")
369
-
370
- img_file = st.camera_input("Take a Snapshot")
371
-
372
- if img_file:
373
- file_bytes = np.asarray(bytearray(img_file.read()), dtype=np.uint8)
374
- frame = cv2.imdecode(file_bytes, 1)
375
- frame = cv2.flip(frame, 1)
376
-
377
- V_raw, msg = process_frame_to_potential(frame)
378
-
379
- if V_raw is not None:
380
- st.success(f"Detected: {msg}")
381
- st.session_state.V_user_defined = V_raw
382
-
383
- # Map to simulation grid
384
- V_interpolated = np.interp(
385
- np.linspace(0, 1, len(x_internal)),
386
- np.linspace(0, 1, len(V_raw)),
387
- V_raw
388
- )
389
- V_physics = V_interpolated * 200.0
390
- V_full_to_solve = np.pad(V_physics, (1,1), constant_values=1e10)
391
- status_msg = f"Camera Potential: {msg}"
392
- else:
393
- st.error(msg)
394
-
395
-
396
- # --------- RIGHT COLUMN: SOLVE & PLOT ----------
397
- with col2:
398
- if V_full_to_solve is not None:
399
- start_time = time.time()
400
- T = f.kinetic_operator(len(x_internal), dx)
401
- E, psi = f.solve(T, V_full_to_solve, dx)
402
- solve_time = time.time() - start_time
403
-
404
- if status_msg:
405
- st.markdown(f"**Potential:** {status_msg}")
406
- st.markdown(f"**Solver Status:** ✅ Converged in {solve_time:.3f} s")
407
-
408
- fig = plot_interactive(E, psi, V_full_to_solve, x_full, nos=nos_user)
409
- st.plotly_chart(fig, use_container_width=True)
410
-
411
- # Eigenenergies panel
412
- st.markdown("### Eigenenergies")
413
- cols = st.columns(nos_user)
414
- for i in range(nos_user):
415
- if i < len(E):
416
- cols[i].metric(f"n={i}", f"{E[i]:.4f} Ha")
417
- else:
418
- if potential_mode == "Hand Gesture (Camera)":
419
- st.info("Follow the instructions on the left to capture a potential from your hands.")
420
- else:
421
- st.info("Select parameters on the left to generate a potential and solve.")
422
-
423
- # ==========================================
424
- # 5. PAGE: BENCHMARKS
425
- # ==========================================
426
- elif page == "Benchmarks & Verification":
427
- st.title("🛡️ Verification & Accuracy")
428
- st.markdown("""
429
- This solver has been rigorously tested against known analytical solutions and external libraries to ensure physical accuracy.
430
- """)
431
-
432
- tab1, tab2, tab3 = st.tabs(["Analytical Benchmarks", "QMSolve Comparison", "Code"])
433
-
434
- with tab1:
435
- st.subheader("1. Infinite Square Well")
436
- st.markdown("Particle in a box of length $L=20$. Error < 0.003%.")
437
- st.table({
438
- "State (n)": [1, 2, 3, 4, 5],
439
- "Analytic E": [0.012337, 0.049348, 0.111033, 0.197392, 0.308425],
440
- "Numerical E": [0.012337, 0.049348, 0.111032, 0.197389, 0.308419],
441
- "% Error": ["0.0001%", "0.0003%", "0.0007%", "0.0013%", "0.0021%"]
442
- })
443
-
444
- st.subheader("2. Harmonic Oscillator")
445
- st.markdown("Standard QHO with $k=1$. Error < 0.02%.")
446
- st.table({
447
- "State (n)": [0, 1, 2, 3, 4],
448
- "Analytic E": [0.5, 1.5, 2.5, 3.5, 4.5],
449
- "Numerical E": [0.499980, 1.499902, 2.499746, 3.499512, 4.499200],
450
- "% Error": ["0.0039%", "0.0065%", "0.0101%", "0.0139%", "0.0178%"]
451
- })
452
-
453
- with tab2:
454
- st.subheader("Cross-Verification: Double Well Potential")
455
- st.markdown("""
456
- Comparison with the Python package `QMSolve` for a Double Well potential (no simple analytic solution).
457
- **Agreement within 0.25%**.
458
- """)
459
-
460
- col_a, col_b = st.columns(2)
461
- with col_a:
462
- st.markdown("**Parameters:** $V(x) = 2(x^2 - 1)^2$")
463
- st.table({
464
- "State (n)": [0, 1, 2, 3, 4],
465
- "psi_solve2 (Ha)": [1.400886, 2.092533, 4.455252, 6.917808, 9.872632],
466
- "QMSolve (Ha)": [1.402472, 2.097767, 4.466368, 6.936807, 9.900227],
467
- "% Difference": ["0.11%", "0.25%", "0.25%", "0.27%", "0.28%"]
468
- })
469
- with col_b:
470
- st.info("Note: QMSolve uses eV units. Results were converted to Hartree (1 Ha 27.211 eV) for comparison.")
471
-
472
- with tab3:
473
- st.subheader("Code Verification")
474
- st.code("""
475
- def kinetic_operator(N, dx, hbar=1, m=1):
476
- # 3-point central difference stencil for 2nd derivative
477
- main_diagonal = (1/dx**2) * np.diag(-2 * np.ones(N))
478
- off_diagonal1 = (1/dx**2) * np.diag(np.ones(N-1), -1)
479
- off_diagonal2 = (1/dx**2) * np.diag(np.ones(N-1), 1)
480
- D2 = (main_diagonal + off_diagonal1 + off_diagonal2)
481
-
482
- # Kinetic Energy Operator T = -hbar^2 / 2m * d^2/dx^2
483
- T = (-(hbar**2 / (2*m)) * D2)
484
- return T
485
- """, language="python")
486
- st.code("""
487
- def harmonic(x,k,center=0.0):
488
- # A Parabola, setting the global k-value.
489
- global Last_k_value
490
- Last_k_value = k
491
-
492
- constant_factor = 1
493
- potential = 0.5*k*(x - center)**2
494
- return constant_factor * potential
495
- """)
496
-
497
-
498
-
499
- # ==========================================
500
- # 6. PAGE: THEORY
501
- # ==========================================
502
- elif page == "Theory & Method":
503
- st.title("📖 Theory & Methodology")
504
-
505
- st.markdown("### The Time-Independent Schrödinger Equation")
506
- st.latex(r" \hat{H}\psi(x) = E\psi(x) ")
507
- st.latex(r" \left[ -\frac{\hbar^2}{2m}\frac{d^2}{dx^2} + V(x) \right]\psi(x) = E\psi(x) ")
508
-
509
- st.markdown("### Numerical Method: Finite Difference")
510
- st.markdown(r"""
511
- We discretize the spatial domain $x$ into a grid of $N$ points. The second derivative is approximated using the **Central Difference Formula**:
512
- """)
513
- st.latex(r" \frac{d^2\psi}{dx^2} \approx \frac{\psi_{i+1} - 2\psi_i + \psi_{i-1}}{\Delta x^2} ")
514
-
515
- st.markdown(r"""
516
- This transforms the differential operator into a **Tridiagonal Matrix** equation:
517
- """)
518
- st.latex(r" \mathbf{H}\mathbf{\psi} = E\mathbf{\psi} ")
519
-
520
- st.markdown(r"""
521
- Where $\mathbf{H}$ is an $N \times N$ matrix. We then use `numpy.linalg.eigh` to solve for the eigenvalues ($E$) and eigenvectors ($\psi$).
522
- """)
523
-
524
- st.markdown("### Implementation Details")
525
- st.markdown(r"""
526
- - **Grid Size:** Dynamic (default 1000–2000 points)
527
- - **Boundary Conditions:** Dirichlet ($ \psi(0) = \psi(L) = 0 $) via infinite walls at grid edges.
528
- - **Units:** Hartree Atomic Units ($\hbar=1, m=1$).
529
- """)
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ # ==========================================
4
+ # 0. PAGE CONFIGURATION & STYLING
5
+ # ==========================================
6
+ st.set_page_config(
7
+ page_title="Pili-Pili Quantum Solver | Ahilan Kumaresan",
8
+ page_icon="🍟",
9
+ layout="wide",
10
+ initial_sidebar_state="expanded"
11
+ )
12
+
13
+ import numpy as np
14
+ import matplotlib.pyplot as plt
15
+ import math
16
+ import time
17
+
18
+ try:
19
+ import mediapipe as mp
20
+ import cv2
21
+ import plotly.graph_objects as go
22
+ from plotly.subplots import make_subplots
23
+ except ImportError as e:
24
+ st.error(f"CRITICAL ERROR: Failed to import required libraries. {e}")
25
+ st.stop()
26
+
27
+ # Import physics engine
28
+ try:
29
+ import functions as f
30
+ except ImportError as e:
31
+ st.error(f"CRITICAL ERROR: Failed to import physics engine. {e}")
32
+ st.stop()
33
+
34
+
35
+ # ==========================================
36
+ # 0. SESSION STATE (for camera flow)
37
+ # ==========================================
38
+ if 'countdown_finished' not in st.session_state:
39
+ st.session_state.countdown_finished = False
40
+ if 'V_user_defined' not in st.session_state:
41
+ st.session_state.V_user_defined = None
42
+
43
+ # Custom CSS for a professional look
44
+ st.markdown("""
45
+ <style>
46
+ .main {
47
+ background-color: #0e1117;
48
+ }
49
+ .stButton>button {
50
+ width: 100%;
51
+ border-radius: 5px;
52
+ height: 3em;
53
+ background-color: #262730;
54
+ color: white;
55
+ border: 1px solid #4b4b4b;
56
+ }
57
+ .stButton>button:hover {
58
+ border-color: #00ADB5;
59
+ color: #00ADB5;
60
+ }
61
+ h1, h2, h3 {
62
+ color: #00ADB5;
63
+ font-family: 'Helvetica Neue', sans-serif;
64
+ }
65
+ </style>
66
+ """, unsafe_allow_html=True)
67
+
68
+ # ==========================================
69
+ # 1. SIDEBAR: PERSONALIZATION & NAV
70
+ # ==========================================
71
+ with st.sidebar:
72
+ st.title("Quantum Solver 2.0")
73
+ st.caption("v2.1 - HF Fix")
74
+ st.markdown("---")
75
+
76
+ # Navigation
77
+ page = st.radio("Navigation", ["Simulator", "Benchmarks & Verification", "Theory & Method"])
78
+
79
+ st.markdown("---")
80
+
81
+ # Author Profile
82
+ st.markdown("### About Moi")
83
+ st.markdown("""
84
+ **Ahilan Kumaresan**
85
+
86
+ *Aspiring Mathematical & Computational Physicist*
87
+
88
+ Developing Interative and accurate numerical tools for quantum mechanics.
89
+ """)
90
+
91
+ st.info("Verified against Analytical Solutions & QMSolve Package.")
92
+
93
+ # ==========================================
94
+ # 2. HELPER FUNCTIONS (Plotting)
95
+ # ==========================================
96
+ def plot_interactive(E, psi, V, x, nos=5):
97
+ """
98
+ Creates a professional interactive Plotly chart for wavefunctions and energy levels.
99
+ """
100
+ # Limit states
101
+ states = min(nos, len(E))
102
+
103
+ # Create subplots: Main plot (Potential + Psi) and Side plot (Energy Levels)
104
+ fig = make_subplots(
105
+ rows=1, cols=2,
106
+ column_widths=[0.8, 0.2],
107
+ shared_yaxes=True,
108
+ horizontal_spacing=0.02,
109
+ subplot_titles=("Wavefunctions & Potential", "Energy Spectrum")
110
+ )
111
+
112
+ # Scaling factor for wavefunctions
113
+ if len(E) >= 2:
114
+ scale = (E[1] - E[0]) * 0.4
115
+ else:
116
+ scale = max(E[0] * 0.1, 0.5)
117
+
118
+ max_E = E[states-1] if states > 0 else 10
119
+ window_height = max_E * 1.5
120
+
121
+ # Get x coordinates for internal points (matching psi dimensions)
122
+ x_internal = x[1:-1]
123
+ V_internal = V[1:-1]
124
+
125
+ # 1. Plot Potential V(x) - using internal points for better visibility
126
+ V_clipped = np.clip(V_internal, 0, window_height)
127
+
128
+ fig.add_trace(
129
+ go.Scatter(
130
+ x=x_internal.tolist() if hasattr(x_internal, 'tolist') else x_internal,
131
+ y=V_clipped.tolist() if hasattr(V_clipped, 'tolist') else V_clipped,
132
+ mode='lines',
133
+ name='V(x)',
134
+ line=dict(color='#FFFFFF', width=2.5),
135
+ hovertemplate='V(x): %{y:.2f}<extra></extra>'
136
+ ),
137
+ row=1, col=1
138
+ )
139
+
140
+ # 2. Plot Wavefunctions (shifted by Energy)
141
+ colors = ['#00ADB5', '#FF2E63', '#F38181', '#FCE38A', '#EAFFD0',
142
+ '#95E1D3', '#FFB6C1', '#DDA0DD', '#87CEEB', '#98FB98']
143
+
144
+ for n in range(states):
145
+ # Normalize wavefunction amplitude
146
+ psi_n = psi[:, n]
147
+ max_amp = np.max(np.abs(psi_n))
148
+ if max_amp > 1e-9:
149
+ psi_n = psi_n / max_amp
150
+ else:
151
+ psi_n = psi_n
152
+
153
+ # Shift by energy
154
+ y_shifted = psi_n * scale + E[n]
155
+
156
+ # Hide where potential is infinite
157
+ y_shifted[V_internal > 1e5] = np.nan
158
+
159
+ color = colors[n % len(colors)]
160
+
161
+ # Ensure arrays match in length
162
+ if len(x_internal) != len(y_shifted):
163
+ # Fallback: truncate to minimum length
164
+ min_len = min(len(x_internal), len(y_shifted))
165
+ x_plot = x_internal[:min_len]
166
+ y_plot = y_shifted[:min_len]
167
+ else:
168
+ x_plot = x_internal
169
+ y_plot = y_shifted
170
+
171
+ fig.add_trace(
172
+ go.Scatter(
173
+ x=x_plot.tolist() if hasattr(x_plot, 'tolist') else x_plot,
174
+ y=y_plot.tolist() if hasattr(y_plot, 'tolist') else y_plot,
175
+ mode='lines',
176
+ name=f'n={n+1}, E={E[n]:.4f}',
177
+ line=dict(color=color, width=2),
178
+ hovertemplate=f'n={n+1}<br>E={E[n]:.4f}<br>x: %{{x:.2f}}<br>ψ: %{{y:.2f}}<extra></extra>'
179
+ ),
180
+ row=1, col=1
181
+ )
182
+
183
+ # Add Energy Level to Side Bar
184
+ fig.add_trace(
185
+ go.Scatter(
186
+ x=[0, 1], y=[E[n], E[n]],
187
+ mode='lines',
188
+ line=dict(color=color, width=3),
189
+ showlegend=False,
190
+ hovertemplate=f'E_{n+1}={E[n]:.4f}<extra></extra>'
191
+ ),
192
+ row=1, col=2
193
+ )
194
+
195
+ # Layout Styling - Enhanced dark mode
196
+ fig.update_layout(
197
+ template="plotly_dark",
198
+ height=600,
199
+ margin=dict(l=20, r=20, t=50, b=20),
200
+ legend=dict(
201
+ orientation="h",
202
+ yanchor="bottom",
203
+ y=1.02,
204
+ xanchor="right",
205
+ x=1,
206
+ font=dict(size=10)
207
+ ),
208
+ hovermode="closest",
209
+ plot_bgcolor='#0e1117',
210
+ paper_bgcolor='#0e1117',
211
+ font=dict(color='#FAFAFA')
212
+ )
213
+
214
+ fig.update_xaxes(
215
+ title_text="Position (a.u.)",
216
+ row=1, col=1,
217
+ gridcolor='#2a2a2a',
218
+ showgrid=True
219
+ )
220
+ fig.update_xaxes(
221
+ showticklabels=False,
222
+ row=1, col=2,
223
+ showgrid=False
224
+ )
225
+ fig.update_yaxes(
226
+ title_text="Energy (Hartree)",
227
+ range=[0, max_E * 1.2],
228
+ row=1, col=1,
229
+ gridcolor='#2a2a2a',
230
+ showgrid=True
231
+ )
232
+
233
+ return fig
234
+
235
+ # ==========================================
236
+ # 3. HELPER: MediaPipe hand → 1D potential
237
+ # ==========================================
238
+ def process_frame_to_potential(frame):
239
+ """
240
+ Takes a BGR frame (OpenCV) and returns:
241
+ pot_profile: 1D array in [0,1] representing V(x) profile
242
+ msg: human-friendly label
243
+ Modes:
244
+ - 2 handsSquare well (0 inside, 1 outside)
245
+ - 1 hand → QHO-like parabola
246
+ """
247
+ try:
248
+ mp_hands = mp.solutions.hands
249
+ with mp_hands.Hands(max_num_hands=2, min_detection_confidence=0.5) as hands:
250
+ h, w, _ = frame.shape
251
+ rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
252
+ res = hands.process(rgb)
253
+
254
+ if not res.multi_hand_landmarks:
255
+ return None, "No Hands Detected, But Cute Smile :)"
256
+
257
+ # --- LOGIC: Square Well vs QHO ---
258
+
259
+ # 1. Square Well (2 Hands)
260
+ if len(res.multi_hand_landmarks) >= 2:
261
+ INDEX_TIP_ID = 8
262
+ x_coords = [lm.landmark[INDEX_TIP_ID].x * w for lm in res.multi_hand_landmarks]
263
+ x_coords.sort()
264
+
265
+ xL_hand, xR_hand = x_coords[0], x_coords[1]
266
+ well_width = xR_hand - xL_hand
267
+
268
+ center_screen = w / 2
269
+ centered_L = center_screen - (well_width / 2)
270
+ centered_R = center_screen + (well_width / 2)
271
+
272
+ x_space = np.linspace(0, w, 400)
273
+ pot_profile = np.ones_like(x_space)
274
+ pot_profile[(x_space > centered_L) & (x_space < centered_R)] = 0
275
+
276
+ return pot_profile, "Square Well (Captured)"
277
+
278
+ # 2. Harmonic Oscillator (1 Hand)
279
+ elif len(res.multi_hand_landmarks) == 1:
280
+ lm = res.multi_hand_landmarks[0]
281
+ THUMB = lm.landmark[4]
282
+ INDEX = lm.landmark[8]
283
+
284
+ dx = INDEX.x - THUMB.x
285
+ dy = INDEX.y - THUMB.y
286
+ dist = math.sqrt(dx**2 + dy**2)
287
+
288
+ # Map pinch distance curvature
289
+ A = np.interp(dist, [0.05, 0.3], [100.0, 1.0])
290
+
291
+ x_space = np.linspace(-1, 1, 400)
292
+ pot_profile = A * (x_space**2)
293
+
294
+ pot_profile = np.clip(pot_profile, 0, 100)
295
+ pot_profile = pot_profile / 100.0 # normalize 0..1
296
+
297
+ return pot_profile, f"Harmonic Oscillator (k={A:.1f})"
298
+
299
+ except Exception as e:
300
+ return None, f"MediaPipe Error: {e}"
301
+
302
+ return None, "Error"
303
+
304
+ # ==========================================
305
+ # 4. PAGE: SIMULATOR
306
+ # ==========================================
307
+ if page == "Simulator":
308
+ st.title("Pili-Pili - Quantum Potential Solver")
309
+ st.markdown("Show a potential with your hands or select a preset to solve the **Time-Independent Schrödinger Equation**.")
310
+
311
+ # Shared grid for all modes
312
+ L = 50
313
+ N_GRID = 1000
314
+ x_full, dx, x_internal = f.make_grid(L, N_GRID)
315
+
316
+ V_full_to_solve = None
317
+ status_msg = ""
318
+
319
+ col1, col2 = st.columns([1, 3])
320
+
321
+ with col1:
322
+ st.subheader("Controls")
323
+
324
+ # Settings
325
+ potential_mode = st.selectbox(
326
+ "Potential Type",
327
+ [
328
+ "Static Square Well",
329
+ "Static Harmonic Oscillator",
330
+ "Double Well",
331
+ "Hand Gesture (Camera)"
332
+ ]
333
+ )
334
+
335
+ nos_user = st.slider("Eigenstates to Plot", 1, 10, 5)
336
+
337
+ # ---- STATIC MODES ----
338
+ if potential_mode == "Static Square Well":
339
+ width = st.slider("Well Width", 1.0, 20.0, 10.0)
340
+ V_physics = np.zeros_like(x_internal)
341
+ V_physics[np.abs(x_internal) > width/2] = 200
342
+ V_full_to_solve = np.pad(V_physics, (1,1), constant_values=1e10)
343
+ status_msg = f"Static Square Well (width = {width:.1f})"
344
+
345
+ elif potential_mode == "Static Harmonic Oscillator":
346
+ k = st.slider("Spring Constant (k)", 0.1, 50.0, 5.0)
347
+ V_physics = 0.5 * k * x_internal**2
348
+ # scale a bit so it shows nicely under energies
349
+ V_physics = V_physics / np.max(V_physics) * 50
350
+ V_full_to_solve = np.pad(V_physics, (1,1), constant_values=1e10)
351
+ status_msg = f"Static Harmonic Oscillator (k = {k:.2f})"
352
+
353
+ elif potential_mode == "Double Well":
354
+ sep = st.slider("Separation", 0.5, 5.0, 2.0)
355
+ depth = st.slider("Depth", 0.1, 5.0, 1.0)
356
+ V_physics = depth * ((x_internal**2 - sep**2)**2)
357
+ V_physics = V_physics / np.max(V_physics) * 50
358
+ V_full_to_solve = np.pad(V_physics, (1,1), constant_values=1e10)
359
+ status_msg = f"Double Well (sep = {sep:.2f}, depth = {depth:.2f})"
360
+
361
+ # ---- HAND-GESTURE / CAMERA MODE ----
362
+ elif potential_mode == "Hand Gesture (Camera)":
363
+ st.subheader("Hand Gesture Controls")
364
+ st.info(
365
+ "1. Click **'Start Countdown'**. (IGNORE)\n"
366
+ "2. Get your **two hands** ready for a Square Well, "
367
+ "or **one-hand pinch** for a Harmonic Oscillator.\n"
368
+ "3. When you'r ready, use **'Take a snapshot'**."
369
+ )
370
+
371
+
372
+ st.subheader("Hand Gesture Input")
373
+
374
+ img_file = st.camera_input("Take a Snapshot")
375
+
376
+ if img_file:
377
+ file_bytes = np.asarray(bytearray(img_file.read()), dtype=np.uint8)
378
+ frame = cv2.imdecode(file_bytes, 1)
379
+ frame = cv2.flip(frame, 1)
380
+
381
+ V_raw, msg = process_frame_to_potential(frame)
382
+
383
+ if V_raw is not None:
384
+ st.success(f"Detected: {msg}")
385
+ st.session_state.V_user_defined = V_raw
386
+
387
+ # Map to simulation grid
388
+ V_interpolated = np.interp(
389
+ np.linspace(0, 1, len(x_internal)),
390
+ np.linspace(0, 1, len(V_raw)),
391
+ V_raw
392
+ )
393
+ V_physics = V_interpolated * 200.0
394
+ V_full_to_solve = np.pad(V_physics, (1,1), constant_values=1e10)
395
+ status_msg = f"Camera Potential: {msg}"
396
+ else:
397
+ st.error(msg)
398
+
399
+
400
+ # --------- RIGHT COLUMN: SOLVE & PLOT ----------
401
+ with col2:
402
+ if V_full_to_solve is not None:
403
+ start_time = time.time()
404
+ T = f.kinetic_operator(len(x_internal), dx)
405
+ E, psi = f.solve(T, V_full_to_solve, dx)
406
+ solve_time = time.time() - start_time
407
+
408
+ if status_msg:
409
+ st.markdown(f"**Potential:** {status_msg}")
410
+ st.markdown(f"**Solver Status:** ✅ Converged in {solve_time:.3f} s")
411
+
412
+ fig = plot_interactive(E, psi, V_full_to_solve, x_full, nos=nos_user)
413
+ st.plotly_chart(fig, use_container_width=True)
414
+
415
+ # Eigenenergies panel
416
+ st.markdown("### Eigenenergies")
417
+ cols = st.columns(nos_user)
418
+ for i in range(nos_user):
419
+ if i < len(E):
420
+ cols[i].metric(f"n={i}", f"{E[i]:.4f} Ha")
421
+ else:
422
+ if potential_mode == "Hand Gesture (Camera)":
423
+ st.info("Follow the instructions on the left to capture a potential from your hands.")
424
+ else:
425
+ st.info("Select parameters on the left to generate a potential and solve.")
426
+
427
+ # ==========================================
428
+ # 5. PAGE: BENCHMARKS
429
+ # ==========================================
430
+ elif page == "Benchmarks & Verification":
431
+ st.title("🛡️ Verification & Accuracy")
432
+ st.markdown("""
433
+ This solver has been rigorously tested against known analytical solutions and external libraries to ensure physical accuracy.
434
+ """)
435
+
436
+ tab1, tab2, tab3 = st.tabs(["Analytical Benchmarks", "QMSolve Comparison", "Code"])
437
+
438
+ with tab1:
439
+ st.subheader("1. Infinite Square Well")
440
+ st.markdown("Particle in a box of length $L=20$. Error < 0.003%.")
441
+ st.table({
442
+ "State (n)": [1, 2, 3, 4, 5],
443
+ "Analytic E": [0.012337, 0.049348, 0.111033, 0.197392, 0.308425],
444
+ "Numerical E": [0.012337, 0.049348, 0.111032, 0.197389, 0.308419],
445
+ "% Error": ["0.0001%", "0.0003%", "0.0007%", "0.0013%", "0.0021%"]
446
+ })
447
+
448
+ st.subheader("2. Harmonic Oscillator")
449
+ st.markdown("Standard QHO with $k=1$. Error < 0.02%.")
450
+ st.table({
451
+ "State (n)": [0, 1, 2, 3, 4],
452
+ "Analytic E": [0.5, 1.5, 2.5, 3.5, 4.5],
453
+ "Numerical E": [0.499980, 1.499902, 2.499746, 3.499512, 4.499200],
454
+ "% Error": ["0.0039%", "0.0065%", "0.0101%", "0.0139%", "0.0178%"]
455
+ })
456
+
457
+ with tab2:
458
+ st.subheader("Cross-Verification: Double Well Potential")
459
+ st.markdown("""
460
+ Comparison with the Python package `QMSolve` for a Double Well potential (no simple analytic solution).
461
+ **Agreement within 0.25%**.
462
+ """)
463
+
464
+ col_a, col_b = st.columns(2)
465
+ with col_a:
466
+ st.markdown("**Parameters:** $V(x) = 2(x^2 - 1)^2$")
467
+ st.table({
468
+ "State (n)": [0, 1, 2, 3, 4],
469
+ "psi_solve2 (Ha)": [1.400886, 2.092533, 4.455252, 6.917808, 9.872632],
470
+ "QMSolve (Ha)": [1.402472, 2.097767, 4.466368, 6.936807, 9.900227],
471
+ "% Difference": ["0.11%", "0.25%", "0.25%", "0.27%", "0.28%"]
472
+ })
473
+ with col_b:
474
+ st.info("Note: QMSolve uses eV units. Results were converted to Hartree (1 Ha ≈ 27.211 eV) for comparison.")
475
+
476
+ with tab3:
477
+ st.subheader("Code Verification")
478
+ st.code("""
479
+ def kinetic_operator(N, dx, hbar=1, m=1):
480
+ # 3-point central difference stencil for 2nd derivative
481
+ main_diagonal = (1/dx**2) * np.diag(-2 * np.ones(N))
482
+ off_diagonal1 = (1/dx**2) * np.diag(np.ones(N-1), -1)
483
+ off_diagonal2 = (1/dx**2) * np.diag(np.ones(N-1), 1)
484
+ D2 = (main_diagonal + off_diagonal1 + off_diagonal2)
485
+
486
+ # Kinetic Energy Operator T = -hbar^2 / 2m * d^2/dx^2
487
+ T = (-(hbar**2 / (2*m)) * D2)
488
+ return T
489
+ """, language="python")
490
+ st.code("""
491
+ def harmonic(x,k,center=0.0):
492
+ # A Parabola, setting the global k-value.
493
+ global Last_k_value
494
+ Last_k_value = k
495
+
496
+ constant_factor = 1
497
+ potential = 0.5*k*(x - center)**2
498
+ return constant_factor * potential
499
+ """)
500
+
501
+
502
+
503
+ # ==========================================
504
+ # 6. PAGE: THEORY
505
+ # ==========================================
506
+ elif page == "Theory & Method":
507
+ st.title("📖 Theory & Methodology")
508
+
509
+ st.markdown("### The Time-Independent Schrödinger Equation")
510
+ st.latex(r" \hat{H}\psi(x) = E\psi(x) ")
511
+ st.latex(r" \left[ -\frac{\hbar^2}{2m}\frac{d^2}{dx^2} + V(x) \right]\psi(x) = E\psi(x) ")
512
+
513
+ st.markdown("### Numerical Method: Finite Difference")
514
+ st.markdown(r"""
515
+ We discretize the spatial domain $x$ into a grid of $N$ points. The second derivative is approximated using the **Central Difference Formula**:
516
+ """)
517
+ st.latex(r" \frac{d^2\psi}{dx^2} \approx \frac{\psi_{i+1} - 2\psi_i + \psi_{i-1}}{\Delta x^2} ")
518
+
519
+ st.markdown(r"""
520
+ This transforms the differential operator into a **Tridiagonal Matrix** equation:
521
+ """)
522
+ st.latex(r" \mathbf{H}\mathbf{\psi} = E\mathbf{\psi} ")
523
+
524
+ st.markdown(r"""
525
+ Where $\mathbf{H}$ is an $N \times N$ matrix. We then use `numpy.linalg.eigh` to solve for the eigenvalues ($E$) and eigenvectors ($\psi$).
526
+ """)
527
+
528
+ st.markdown("### Implementation Details")
529
+ st.markdown(r"""
530
+ - **Grid Size:** Dynamic (default 1000–2000 points)
531
+ - **Boundary Conditions:** Dirichlet ($ \psi(0) = \psi(L) = 0 $) via infinite walls at grid edges.
532
+ - **Units:** Hartree Atomic Units ($\hbar=1, m=1$).
533
+ """)