Skip to main content

proof_engine/quantum/
schrodinger.rs

1use std::f64::consts::PI;
2
3/// Complex number with full arithmetic.
4#[derive(Clone, Copy, Debug, PartialEq)]
5pub struct Complex {
6    pub re: f64,
7    pub im: f64,
8}
9
10impl Complex {
11    pub fn new(re: f64, im: f64) -> Self {
12        Self { re, im }
13    }
14
15    pub fn zero() -> Self {
16        Self { re: 0.0, im: 0.0 }
17    }
18
19    pub fn one() -> Self {
20        Self { re: 1.0, im: 0.0 }
21    }
22
23    pub fn i() -> Self {
24        Self { re: 0.0, im: 1.0 }
25    }
26
27    pub fn norm_sq(&self) -> f64 {
28        self.re * self.re + self.im * self.im
29    }
30
31    pub fn norm(&self) -> f64 {
32        self.norm_sq().sqrt()
33    }
34
35    pub fn conj(&self) -> Self {
36        Self { re: self.re, im: -self.im }
37    }
38
39    pub fn exp(self) -> Self {
40        let e = self.re.exp();
41        Self {
42            re: e * self.im.cos(),
43            im: e * self.im.sin(),
44        }
45    }
46
47    pub fn from_polar(r: f64, theta: f64) -> Self {
48        Self {
49            re: r * theta.cos(),
50            im: r * theta.sin(),
51        }
52    }
53
54    pub fn arg(&self) -> f64 {
55        self.im.atan2(self.re)
56    }
57
58    pub fn scale(self, s: f64) -> Self {
59        Self { re: self.re * s, im: self.im * s }
60    }
61}
62
63impl std::ops::Add for Complex {
64    type Output = Self;
65    fn add(self, rhs: Self) -> Self {
66        Self { re: self.re + rhs.re, im: self.im + rhs.im }
67    }
68}
69
70impl std::ops::AddAssign for Complex {
71    fn add_assign(&mut self, rhs: Self) {
72        self.re += rhs.re;
73        self.im += rhs.im;
74    }
75}
76
77impl std::ops::Sub for Complex {
78    type Output = Self;
79    fn sub(self, rhs: Self) -> Self {
80        Self { re: self.re - rhs.re, im: self.im - rhs.im }
81    }
82}
83
84impl std::ops::SubAssign for Complex {
85    fn sub_assign(&mut self, rhs: Self) {
86        self.re -= rhs.re;
87        self.im -= rhs.im;
88    }
89}
90
91impl std::ops::Mul for Complex {
92    type Output = Self;
93    fn mul(self, rhs: Self) -> Self {
94        Self {
95            re: self.re * rhs.re - self.im * rhs.im,
96            im: self.re * rhs.im + self.im * rhs.re,
97        }
98    }
99}
100
101impl std::ops::MulAssign for Complex {
102    fn mul_assign(&mut self, rhs: Self) {
103        let re = self.re * rhs.re - self.im * rhs.im;
104        let im = self.re * rhs.im + self.im * rhs.re;
105        self.re = re;
106        self.im = im;
107    }
108}
109
110impl std::ops::Div for Complex {
111    type Output = Self;
112    fn div(self, rhs: Self) -> Self {
113        let d = rhs.norm_sq();
114        Self {
115            re: (self.re * rhs.re + self.im * rhs.im) / d,
116            im: (self.im * rhs.re - self.re * rhs.im) / d,
117        }
118    }
119}
120
121impl std::ops::Neg for Complex {
122    type Output = Self;
123    fn neg(self) -> Self {
124        Self { re: -self.re, im: -self.im }
125    }
126}
127
128impl std::ops::Mul<f64> for Complex {
129    type Output = Self;
130    fn mul(self, rhs: f64) -> Self {
131        Self { re: self.re * rhs, im: self.im * rhs }
132    }
133}
134
135impl std::ops::Mul<Complex> for f64 {
136    type Output = Complex;
137    fn mul(self, rhs: Complex) -> Complex {
138        Complex { re: self * rhs.re, im: self * rhs.im }
139    }
140}
141
142impl std::ops::Div<f64> for Complex {
143    type Output = Self;
144    fn div(self, rhs: f64) -> Self {
145        Self { re: self.re / rhs, im: self.im / rhs }
146    }
147}
148
149impl Default for Complex {
150    fn default() -> Self {
151        Self::zero()
152    }
153}
154
155/// 1D wave function on a uniform grid.
156#[derive(Clone, Debug)]
157pub struct WaveFunction1D {
158    pub psi: Vec<Complex>,
159    pub dx: f64,
160    pub x_min: f64,
161}
162
163impl WaveFunction1D {
164    pub fn new(psi: Vec<Complex>, dx: f64, x_min: f64) -> Self {
165        Self { psi, dx, x_min }
166    }
167
168    pub fn n(&self) -> usize {
169        self.psi.len()
170    }
171
172    pub fn x_at(&self, i: usize) -> f64 {
173        self.x_min + i as f64 * self.dx
174    }
175
176    pub fn x_max(&self) -> f64 {
177        self.x_min + (self.n() - 1) as f64 * self.dx
178    }
179
180    pub fn probability_density(&self) -> Vec<f64> {
181        self.psi.iter().map(|c| c.norm_sq()).collect()
182    }
183
184    pub fn norm_squared(&self) -> f64 {
185        self.psi.iter().map(|c| c.norm_sq()).sum::<f64>() * self.dx
186    }
187
188    pub fn normalize(&mut self) {
189        let n = self.norm_squared().sqrt();
190        if n > 1e-30 {
191            for c in &mut self.psi {
192                *c = *c / n;
193            }
194        }
195    }
196}
197
198/// Solve tridiagonal system Ax = d where A has diagonals (a, b, c).
199/// a[0] and c[n-1] are unused. Modifies d in-place and returns solution.
200fn solve_tridiagonal(a: &[Complex], b: &[Complex], c: &[Complex], d: &mut [Complex]) -> Vec<Complex> {
201    let n = d.len();
202    let mut cp = vec![Complex::zero(); n];
203    let mut dp = vec![Complex::zero(); n];
204
205    cp[0] = c[0] / b[0];
206    dp[0] = d[0] / b[0];
207
208    for i in 1..n {
209        let m = b[i] - a[i] * cp[i - 1];
210        cp[i] = if i < n - 1 { c[i] / m } else { Complex::zero() };
211        dp[i] = (d[i] - a[i] * dp[i - 1]) / m;
212    }
213
214    let mut x = vec![Complex::zero(); n];
215    x[n - 1] = dp[n - 1];
216    for i in (0..n - 1).rev() {
217        x[i] = dp[i] - cp[i] * x[i + 1];
218    }
219    x
220}
221
222/// 1D Schrodinger equation solver using Crank-Nicolson and split-operator methods.
223#[derive(Clone)]
224pub struct SchrodingerSolver1D {
225    pub psi: WaveFunction1D,
226    pub potential: Vec<f64>,
227    pub mass: f64,
228    pub hbar: f64,
229    pub dt: f64,
230}
231
232impl SchrodingerSolver1D {
233    pub fn new(psi: WaveFunction1D, potential: Vec<f64>, mass: f64, hbar: f64, dt: f64) -> Self {
234        Self { psi, potential, mass, hbar, dt }
235    }
236
237    /// Crank-Nicolson time step (implicit, unitary).
238    pub fn step(&mut self) {
239        let n = self.psi.n();
240        let dx = self.psi.dx;
241        let r = Complex::new(0.0, self.hbar * self.dt / (4.0 * self.mass * dx * dx));
242
243        let mut a_lower = vec![Complex::zero(); n];
244        let mut a_diag = vec![Complex::zero(); n];
245        let mut a_upper = vec![Complex::zero(); n];
246        let mut rhs = vec![Complex::zero(); n];
247
248        for j in 0..n {
249            let v_term = Complex::new(0.0, self.dt * self.potential[j] / (2.0 * self.hbar));
250
251            // LHS: (1 + iHdt/2)
252            a_diag[j] = Complex::one() + r * 2.0 + v_term;
253            if j > 0 {
254                a_lower[j] = -r;
255            }
256            if j < n - 1 {
257                a_upper[j] = -r;
258            }
259
260            // RHS: (1 - iHdt/2) * psi
261            let psi_j = self.psi.psi[j];
262            let psi_left = if j > 0 { self.psi.psi[j - 1] } else { Complex::zero() };
263            let psi_right = if j < n - 1 { self.psi.psi[j + 1] } else { Complex::zero() };
264
265            rhs[j] = (Complex::one() - r * 2.0 - v_term) * psi_j
266                + r * psi_left
267                + r * psi_right;
268        }
269
270        self.psi.psi = solve_tridiagonal(&a_lower, &a_diag, &a_upper, &mut rhs);
271    }
272
273    /// Split-operator FFT method: kinetic in k-space, potential in x-space.
274    pub fn step_split_operator(&mut self) {
275        let n = self.psi.n();
276        let dx = self.psi.dx;
277        let dt = self.dt;
278        let hbar = self.hbar;
279        let mass = self.mass;
280
281        // Half potential step in x-space
282        for j in 0..n {
283            let phase = -self.potential[j] * dt / (2.0 * hbar);
284            let exp_v = Complex::from_polar(1.0, phase);
285            self.psi.psi[j] = self.psi.psi[j] * exp_v;
286        }
287
288        // Full kinetic step in k-space
289        let mut psi_k = dft(&self.psi.psi);
290        let dk = 2.0 * PI / (n as f64 * dx);
291        for j in 0..n {
292            let k = if j <= n / 2 {
293                j as f64 * dk
294            } else {
295                (j as f64 - n as f64) * dk
296            };
297            let phase = -hbar * k * k * dt / (2.0 * mass);
298            let exp_t = Complex::from_polar(1.0, phase);
299            psi_k[j] = psi_k[j] * exp_t;
300        }
301        self.psi.psi = idft(&psi_k);
302
303        // Half potential step in x-space
304        for j in 0..n {
305            let phase = -self.potential[j] * dt / (2.0 * hbar);
306            let exp_v = Complex::from_polar(1.0, phase);
307            self.psi.psi[j] = self.psi.psi[j] * exp_v;
308        }
309    }
310}
311
312/// Discrete Fourier Transform.
313pub fn dft(input: &[Complex]) -> Vec<Complex> {
314    let n = input.len();
315    let mut output = vec![Complex::zero(); n];
316    for k in 0..n {
317        let mut sum = Complex::zero();
318        for j in 0..n {
319            let angle = -2.0 * PI * (k as f64) * (j as f64) / (n as f64);
320            sum += input[j] * Complex::from_polar(1.0, angle);
321        }
322        output[k] = sum;
323    }
324    output
325}
326
327/// Inverse Discrete Fourier Transform.
328pub fn idft(input: &[Complex]) -> Vec<Complex> {
329    let n = input.len();
330    let mut output = vec![Complex::zero(); n];
331    for j in 0..n {
332        let mut sum = Complex::zero();
333        for k in 0..n {
334            let angle = 2.0 * PI * (k as f64) * (j as f64) / (n as f64);
335            sum += input[k] * Complex::from_polar(1.0, angle);
336        }
337        output[j] = sum / n as f64;
338    }
339    output
340}
341
342/// Compute energy eigenvalues using the shooting method for the time-independent
343/// Schrodinger equation with the given potential on a uniform grid.
344pub fn energy_eigenvalues(potential: &[f64], n_states: usize, dx: f64, mass: f64, hbar: f64) -> Vec<f64> {
345    let n = potential.len();
346    let v_min = potential.iter().cloned().fold(f64::INFINITY, f64::min);
347    let v_max = potential.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
348
349    let mut eigenvalues = Vec::new();
350    // Energy scan step. It used to be (V_max - V_min) / 10000, so a tall
351    // wall (an "infinite" well of 1e6) made the step 100, larger than the
352    // first several level spacings, and the low levels were skipped. Step
353    // at a fraction of the box's ground-state scale hbar^2 pi^2 / (2 m L^2),
354    // capped so a tall potential still needs at most 200,000 steps.
355    let length = (n.max(2) - 1) as f64 * dx;
356    let e_scale = hbar * hbar * std::f64::consts::PI.powi(2) / (2.0 * mass * length * length);
357    let de = (e_scale / 50.0).max((v_max - v_min + 10.0) / 200_000.0);
358    let mut e = v_min + de;
359    let mut prev_end = shoot(potential, e, dx, mass, hbar);
360
361    while eigenvalues.len() < n_states && e < v_max + 50.0 {
362        e += de;
363        let cur_end = shoot(potential, e, dx, mass, hbar);
364        if prev_end * cur_end < 0.0 {
365            // Sign change: refine with bisection
366            let mut lo = e - de;
367            let mut hi = e;
368            for _ in 0..60 {
369                let mid = (lo + hi) / 2.0;
370                let mid_val = shoot(potential, mid, dx, mass, hbar);
371                if mid_val * shoot(potential, lo, dx, mass, hbar) < 0.0 {
372                    hi = mid;
373                } else {
374                    lo = mid;
375                }
376            }
377            eigenvalues.push((lo + hi) / 2.0);
378        }
379        prev_end = cur_end;
380    }
381    eigenvalues
382}
383
384/// Shooting method: integrate Schrodinger equation from left to right and return
385/// the value of psi at the right boundary. A zero crossing indicates an eigenvalue.
386fn shoot(potential: &[f64], energy: f64, dx: f64, mass: f64, hbar: f64) -> f64 {
387    let n = potential.len();
388    let coeff = 2.0 * mass / (hbar * hbar);
389    let mut psi_prev = 0.0_f64;
390    let mut psi_curr = 1e-10_f64;
391
392    for i in 1..n - 1 {
393        let k_sq = coeff * (potential[i] - energy);
394        let psi_next = 2.0 * psi_curr - psi_prev + dx * dx * k_sq * psi_curr;
395        psi_prev = psi_curr;
396        psi_curr = psi_next;
397    }
398    psi_curr
399}
400
401/// Compute energy eigenstates using the shooting method.
402pub fn energy_eigenstates(
403    potential: &[f64],
404    n_states: usize,
405    dx: f64,
406    x_min: f64,
407    mass: f64,
408    hbar: f64,
409) -> Vec<WaveFunction1D> {
410    let eigenvalues = energy_eigenvalues(potential, n_states, dx, mass, hbar);
411    let n = potential.len();
412    let coeff = 2.0 * mass / (hbar * hbar);
413
414    eigenvalues
415        .iter()
416        .map(|&energy| {
417            let mut psi = vec![0.0_f64; n];
418            psi[1] = 1e-10;
419            for i in 1..n - 1 {
420                let k_sq = coeff * (potential[i] - energy);
421                psi[i + 1] = 2.0 * psi[i] - psi[i - 1] + dx * dx * k_sq * psi[i];
422            }
423            let psi_c: Vec<Complex> = psi.iter().map(|&v| Complex::new(v, 0.0)).collect();
424            let mut wf = WaveFunction1D::new(psi_c, dx, x_min);
425            wf.normalize();
426            wf
427        })
428        .collect()
429}
430
431/// 2D Schrodinger solver using ADI (alternating direction implicit) method.
432#[derive(Clone)]
433pub struct SchrodingerSolver2D {
434    pub psi: Vec<Vec<Complex>>,
435    pub potential: Vec<Vec<f64>>,
436    pub nx: usize,
437    pub ny: usize,
438    pub dx: f64,
439    pub dy: f64,
440    pub dt: f64,
441    pub mass: f64,
442    pub hbar: f64,
443}
444
445impl SchrodingerSolver2D {
446    pub fn new(
447        psi: Vec<Vec<Complex>>,
448        potential: Vec<Vec<f64>>,
449        nx: usize,
450        ny: usize,
451        dx: f64,
452        dy: f64,
453        dt: f64,
454        mass: f64,
455        hbar: f64,
456    ) -> Self {
457        Self { psi, potential, nx, ny, dx, dy, dt, mass, hbar }
458    }
459
460    /// ADI time step: half step implicit in x, half step implicit in y.
461    pub fn step_2d(&mut self) {
462        let nx = self.nx;
463        let ny = self.ny;
464        let rx = Complex::new(0.0, self.hbar * self.dt / (4.0 * self.mass * self.dx * self.dx));
465        let ry = Complex::new(0.0, self.hbar * self.dt / (4.0 * self.mass * self.dy * self.dy));
466
467        // Half step: implicit in x, explicit in y
468        let mut psi_half = vec![vec![Complex::zero(); ny]; nx];
469        for j in 0..ny {
470            let mut a = vec![Complex::zero(); nx];
471            let mut b = vec![Complex::zero(); nx];
472            let mut c = vec![Complex::zero(); nx];
473            let mut d = vec![Complex::zero(); nx];
474
475            for i in 0..nx {
476                let v_term = Complex::new(0.0, self.dt * self.potential[i][j] / (4.0 * self.hbar));
477                b[i] = Complex::one() + rx * 2.0 + v_term;
478                if i > 0 { a[i] = -rx; }
479                if i < nx - 1 { c[i] = -rx; }
480
481                let psi_ij = self.psi[i][j];
482                let psi_up = if j > 0 { self.psi[i][j - 1] } else { Complex::zero() };
483                let psi_down = if j < ny - 1 { self.psi[i][j + 1] } else { Complex::zero() };
484
485                d[i] = (Complex::one() - ry * 2.0 - v_term) * psi_ij
486                    + ry * psi_up
487                    + ry * psi_down;
488            }
489            let sol = solve_tridiagonal(&a, &b, &c, &mut d);
490            for i in 0..nx {
491                psi_half[i][j] = sol[i];
492            }
493        }
494
495        // Half step: implicit in y, explicit in x
496        for i in 0..nx {
497            let mut a = vec![Complex::zero(); ny];
498            let mut b = vec![Complex::zero(); ny];
499            let mut c = vec![Complex::zero(); ny];
500            let mut d = vec![Complex::zero(); ny];
501
502            for j in 0..ny {
503                let v_term = Complex::new(0.0, self.dt * self.potential[i][j] / (4.0 * self.hbar));
504                b[j] = Complex::one() + ry * 2.0 + v_term;
505                if j > 0 { a[j] = -ry; }
506                if j < ny - 1 { c[j] = -ry; }
507
508                let psi_ij = psi_half[i][j];
509                let psi_left = if i > 0 { psi_half[i - 1][j] } else { Complex::zero() };
510                let psi_right = if i < nx - 1 { psi_half[i + 1][j] } else { Complex::zero() };
511
512                d[j] = (Complex::one() - rx * 2.0 - v_term) * psi_ij
513                    + rx * psi_left
514                    + rx * psi_right;
515            }
516            let sol = solve_tridiagonal(&a, &b, &c, &mut d);
517            for j in 0..ny {
518                self.psi[i][j] = sol[j];
519            }
520        }
521    }
522}
523
524/// Probability density |psi|^2 for 1D wave function.
525pub fn probability_density_1d(psi: &[Complex]) -> Vec<f64> {
526    psi.iter().map(|c| c.norm_sq()).collect()
527}
528
529/// Probability density |psi|^2 for 2D wave function.
530pub fn probability_density_2d(psi: &[Vec<Complex>]) -> Vec<Vec<f64>> {
531    psi.iter()
532        .map(|row| row.iter().map(|c| c.norm_sq()).collect())
533        .collect()
534}
535
536/// Normalize a 1D wave function so that integral |psi|^2 dx = 1.
537pub fn normalize(psi: &mut [Complex], dx: f64) {
538    let norm_sq: f64 = psi.iter().map(|c| c.norm_sq()).sum::<f64>() * dx;
539    let norm = norm_sq.sqrt();
540    if norm > 1e-30 {
541        for c in psi.iter_mut() {
542            *c = *c / norm;
543        }
544    }
545}
546
547/// Expectation value of position: `<x>` = integral psi* x psi dx.
548pub fn expectation_x(psi: &WaveFunction1D) -> f64 {
549    let mut sum = 0.0;
550    for i in 0..psi.n() {
551        let x = psi.x_at(i);
552        sum += psi.psi[i].norm_sq() * x;
553    }
554    sum * psi.dx
555}
556
557/// Expectation value of momentum: `<p>` = -i hbar integral psi* dpsi/dx dx.
558pub fn expectation_p(psi: &WaveFunction1D, hbar: f64) -> f64 {
559    let n = psi.n();
560    let dx = psi.dx;
561    let mut sum = Complex::zero();
562    for i in 1..n - 1 {
563        let dpsi = (psi.psi[i + 1] - psi.psi[i - 1]) / (2.0 * dx);
564        sum += psi.psi[i].conj() * dpsi;
565    }
566    let result = Complex::new(0.0, -hbar) * sum * dx;
567    result.re
568}
569
570/// Uncertainty in position: sqrt(<x^2> - `<x>`^2).
571pub fn uncertainty_x(psi: &WaveFunction1D) -> f64 {
572    let ex = expectation_x(psi);
573    let mut ex2 = 0.0;
574    for i in 0..psi.n() {
575        let x = psi.x_at(i);
576        ex2 += psi.psi[i].norm_sq() * x * x;
577    }
578    ex2 *= psi.dx;
579    (ex2 - ex * ex).max(0.0).sqrt()
580}
581
582/// Uncertainty in momentum: sqrt(<p^2> - `<p>`^2).
583pub fn uncertainty_p(psi: &WaveFunction1D, hbar: f64) -> f64 {
584    let ep = expectation_p(psi, hbar);
585    let n = psi.n();
586    let dx = psi.dx;
587
588    // <p^2> = -hbar^2 integral psi* d^2psi/dx^2 dx
589    let mut sum = 0.0;
590    for i in 1..n - 1 {
591        let d2psi = (psi.psi[i + 1] - psi.psi[i] * 2.0 + psi.psi[i - 1]) / (dx * dx);
592        let integrand = psi.psi[i].conj() * d2psi;
593        sum += integrand.re;
594    }
595    let ep2 = -hbar * hbar * sum * dx;
596    (ep2 - ep * ep).max(0.0).sqrt()
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602
603    #[test]
604    fn test_complex_arithmetic() {
605        let a = Complex::new(1.0, 2.0);
606        let b = Complex::new(3.0, 4.0);
607        let sum = a + b;
608        assert!((sum.re - 4.0).abs() < 1e-10);
609        assert!((sum.im - 6.0).abs() < 1e-10);
610
611        let prod = a * b;
612        assert!((prod.re - (-5.0)).abs() < 1e-10);
613        assert!((prod.im - 10.0).abs() < 1e-10);
614
615        let div = a / b;
616        let expected_re = 11.0 / 25.0;
617        let expected_im = 2.0 / 25.0;
618        assert!((div.re - expected_re).abs() < 1e-10);
619        assert!((div.im - expected_im).abs() < 1e-10);
620    }
621
622    #[test]
623    fn test_complex_exp() {
624        let z = Complex::new(0.0, PI);
625        let result = z.exp();
626        assert!((result.re - (-1.0)).abs() < 1e-10);
627        assert!(result.im.abs() < 1e-10);
628    }
629
630    #[test]
631    fn test_complex_from_polar() {
632        let c = Complex::from_polar(2.0, PI / 4.0);
633        assert!((c.re - 2.0_f64.sqrt()).abs() < 1e-10);
634        assert!((c.im - 2.0_f64.sqrt()).abs() < 1e-10);
635    }
636
637    #[test]
638    fn test_normalization() {
639        let n = 200;
640        let dx = 0.1;
641        let x_min = -10.0;
642        let sigma = 1.0;
643        let psi: Vec<Complex> = (0..n)
644            .map(|i| {
645                let x = x_min + i as f64 * dx;
646                Complex::new((-x * x / (2.0 * sigma * sigma)).exp(), 0.0)
647            })
648            .collect();
649        let mut wf = WaveFunction1D::new(psi, dx, x_min);
650        wf.normalize();
651        let norm = wf.norm_squared();
652        assert!((norm - 1.0).abs() < 0.01);
653    }
654
655    #[test]
656    fn test_crank_nicolson_preserves_norm() {
657        let n = 128;
658        let dx = 0.1;
659        let x_min = -6.4;
660        let sigma = 1.0;
661        let psi: Vec<Complex> = (0..n)
662            .map(|i| {
663                let x = x_min + i as f64 * dx;
664                Complex::new((-x * x / (2.0 * sigma * sigma)).exp(), 0.0)
665            })
666            .collect();
667        let mut wf = WaveFunction1D::new(psi, dx, x_min);
668        wf.normalize();
669        let potential = vec![0.0; n];
670        let mut solver = SchrodingerSolver1D::new(wf, potential, 1.0, 1.0, 0.001);
671        let norm_before = solver.psi.norm_squared();
672        for _ in 0..50 {
673            solver.step();
674        }
675        let norm_after = solver.psi.norm_squared();
676        assert!((norm_after - norm_before).abs() < 0.05);
677    }
678
679    #[test]
680    fn test_split_operator_preserves_norm() {
681        let n = 64;
682        let dx = 0.2;
683        let x_min = -6.4;
684        let sigma = 1.0;
685        let psi: Vec<Complex> = (0..n)
686            .map(|i| {
687                let x = x_min + i as f64 * dx;
688                Complex::new((-x * x / (2.0 * sigma * sigma)).exp(), 0.0)
689            })
690            .collect();
691        let mut wf = WaveFunction1D::new(psi, dx, x_min);
692        wf.normalize();
693        let potential = vec![0.0; n];
694        let mut solver = SchrodingerSolver1D::new(wf, potential, 1.0, 1.0, 0.001);
695        let norm_before = solver.psi.norm_squared();
696        for _ in 0..20 {
697            solver.step_split_operator();
698        }
699        let norm_after = solver.psi.norm_squared();
700        assert!((norm_after - norm_before).abs() < 0.05);
701    }
702
703    #[test]
704    fn test_uncertainty_principle() {
705        let n = 512;
706        let dx = 0.05;
707        let x_min = -12.8;
708        let sigma = 1.0;
709        let hbar = 1.0;
710        let psi: Vec<Complex> = (0..n)
711            .map(|i| {
712                let x = x_min + i as f64 * dx;
713                Complex::new((-x * x / (4.0 * sigma * sigma)).exp(), 0.0)
714            })
715            .collect();
716        let mut wf = WaveFunction1D::new(psi, dx, x_min);
717        wf.normalize();
718        let dx_unc = uncertainty_x(&wf);
719        let dp_unc = uncertainty_p(&wf, hbar);
720        let product = dx_unc * dp_unc;
721        // Heisenberg: dx * dp >= hbar/2
722        assert!(product >= hbar / 2.0 - 0.1, "Uncertainty product {} < hbar/2", product);
723    }
724
725    #[test]
726    fn test_dft_idft_roundtrip() {
727        let input = vec![
728            Complex::new(1.0, 0.0),
729            Complex::new(0.0, 1.0),
730            Complex::new(-1.0, 0.0),
731            Complex::new(0.0, -1.0),
732        ];
733        let transformed = dft(&input);
734        let recovered = idft(&transformed);
735        for (a, b) in input.iter().zip(recovered.iter()) {
736            assert!((a.re - b.re).abs() < 1e-10);
737            assert!((a.im - b.im).abs() < 1e-10);
738        }
739    }
740
741    #[test]
742    fn test_tridiagonal_solver() {
743        // Simple 3x3 system
744        let a = [Complex::zero(), Complex::new(-1.0, 0.0), Complex::new(-1.0, 0.0)];
745        let b = [Complex::new(2.0, 0.0), Complex::new(2.0, 0.0), Complex::new(2.0, 0.0)];
746        let c = [Complex::new(-1.0, 0.0), Complex::new(-1.0, 0.0), Complex::zero()];
747        let mut d = [Complex::new(1.0, 0.0), Complex::new(0.0, 0.0), Complex::new(1.0, 0.0)];
748        let x = solve_tridiagonal(&a, &b, &c, &mut d);
749        // Verify Ax = d
750        let r0 = b[0] * x[0] + c[0] * x[1];
751        assert!((r0.re - 1.0).abs() < 1e-10);
752    }
753
754    #[test]
755    fn test_infinite_well_eigenvalues() {
756        let n = 200;
757        let l = 1.0;
758        let dx = l / (n as f64 - 1.0);
759        let hbar = 1.0;
760        let mass = 0.5; // so hbar^2/(2m) = 1
761        let potential: Vec<f64> = (0..n)
762            .map(|i| {
763                let x = i as f64 * dx;
764                if x < 0.01 || x > l - 0.01 { 1e6 } else { 0.0 }
765            })
766            .collect();
767        let evals = energy_eigenvalues(&potential, 3, dx, mass, hbar);
768        // E_n = n^2 pi^2 hbar^2 / (2 m L^2) = n^2 pi^2
769        if evals.len() >= 2 {
770            let ratio = evals[1] / evals[0];
771            // Should be close to 4 (2^2/1^2)
772            assert!((ratio - 4.0).abs() < 1.0, "Ratio: {}", ratio);
773        }
774        assert_eq!(evals.len(), 3, "should find the three lowest levels");
775    }
776
777    #[test]
778    fn test_2d_solver_runs() {
779        let nx = 16;
780        let ny = 16;
781        let psi = vec![vec![Complex::zero(); ny]; nx];
782        let potential = vec![vec![0.0; ny]; nx];
783        let mut solver = SchrodingerSolver2D::new(psi, potential, nx, ny, 0.1, 0.1, 0.001, 1.0, 1.0);
784        // Place a peak in the center
785        solver.psi[nx / 2][ny / 2] = Complex::new(1.0, 0.0);
786        solver.step_2d();
787        // Just verify it doesn't panic and spreads
788        let center_prob = solver.psi[nx / 2][ny / 2].norm_sq();
789        assert!(center_prob < 1.1); // shouldn't blow up
790    }
791}