Skip to main content

proof_engine/quantum/
wavefunction.rs

1use std::f64::consts::PI;
2use super::schrodinger::{Complex, WaveFunction1D, dft, idft};
3
4/// Render 1D wave function as glyph brightness (|psi|^2) and phase (hue).
5pub struct WaveFunctionRenderer1D {
6    pub width: usize,
7    pub brightness_scale: f64,
8}
9
10impl WaveFunctionRenderer1D {
11    pub fn new(width: usize) -> Self {
12        Self { width, brightness_scale: 1.0 }
13    }
14
15    /// Render the wave function to a vector of (character, r, g, b, brightness).
16    pub fn render(&self, wf: &WaveFunction1D) -> Vec<(char, f64, f64, f64, f64)> {
17        let n = wf.n();
18        let mut result = Vec::with_capacity(self.width);
19        for i in 0..self.width {
20            let idx = (i * n) / self.width.max(1);
21            let idx = idx.min(n - 1);
22            let c = wf.psi[idx];
23            let prob = c.norm_sq() * self.brightness_scale;
24            let phase = c.arg();
25            let (r, g, b) = PhaseColorMap::phase_to_rgb(phase);
26            let ch = brightness_to_char(prob);
27            result.push((ch, r, g, b, prob));
28        }
29        result
30    }
31}
32
33/// Render 2D wave function as glyph brightness grid.
34pub struct WaveFunctionRenderer2D {
35    pub width: usize,
36    pub height: usize,
37    pub brightness_scale: f64,
38}
39
40impl WaveFunctionRenderer2D {
41    pub fn new(width: usize, height: usize) -> Self {
42        Self { width, height, brightness_scale: 1.0 }
43    }
44
45    pub fn render(&self, psi: &[Vec<Complex>]) -> Vec<Vec<(char, f64, f64, f64, f64)>> {
46        let nx = psi.len();
47        let ny = if nx > 0 { psi[0].len() } else { 0 };
48        let mut grid = Vec::with_capacity(self.height);
49        for j in 0..self.height {
50            let mut row = Vec::with_capacity(self.width);
51            for i in 0..self.width {
52                let ix = (i * nx) / self.width.max(1);
53                let iy = (j * ny) / self.height.max(1);
54                let ix = ix.min(nx.saturating_sub(1));
55                let iy = iy.min(ny.saturating_sub(1));
56                if nx == 0 || ny == 0 {
57                    row.push((' ', 0.0, 0.0, 0.0, 0.0));
58                    continue;
59                }
60                let c = psi[ix][iy];
61                let prob = c.norm_sq() * self.brightness_scale;
62                let phase = c.arg();
63                let (r, g, b) = PhaseColorMap::phase_to_rgb(phase);
64                let ch = brightness_to_char(prob);
65                row.push((ch, r, g, b, prob));
66            }
67            grid.push(row);
68        }
69        grid
70    }
71}
72
73fn brightness_to_char(b: f64) -> char {
74    let chars = [' ', '.', ':', '-', '=', '+', '*', '#', '%', '@'];
75    let idx = (b * (chars.len() - 1) as f64).round() as usize;
76    chars[idx.min(chars.len() - 1)]
77}
78
79/// Generate a Gaussian wave packet centered at x0 with momentum k0.
80pub fn gaussian_wavepacket(x0: f64, k0: f64, sigma: f64, x_grid: &[f64]) -> Vec<Complex> {
81    let norm = 1.0 / (sigma * (2.0 * PI).sqrt()).sqrt();
82    x_grid
83        .iter()
84        .map(|&x| {
85            let gauss = (-((x - x0) * (x - x0)) / (4.0 * sigma * sigma)).exp();
86            let phase = k0 * x;
87            Complex::new(norm * gauss * phase.cos(), norm * gauss * phase.sin())
88        })
89        .collect()
90}
91
92/// Generate a plane wave with wave number k.
93pub fn plane_wave(k: f64, x_grid: &[f64]) -> Vec<Complex> {
94    let n = x_grid.len();
95    let norm = 1.0 / (n as f64).sqrt();
96    x_grid
97        .iter()
98        .map(|&x| {
99            let phase = k * x;
100            Complex::new(norm * phase.cos(), norm * phase.sin())
101        })
102        .collect()
103}
104
105/// Generate a coherent state of the harmonic oscillator.
106pub fn coherent_state(n_max: usize, alpha: Complex, x_grid: &[f64], omega: f64, mass: f64, hbar: f64) -> Vec<Complex> {
107    let mut psi = vec![Complex::zero(); x_grid.len()];
108    let alpha_sq = alpha.norm_sq();
109    let prefactor = (-alpha_sq / 2.0).exp();
110
111    for n in 0..n_max {
112        // c_n = alpha^n / sqrt(n!) * exp(-|alpha|^2/2)
113        let mut alpha_n = Complex::one();
114        for _ in 0..n {
115            alpha_n = alpha_n * alpha;
116        }
117        let n_fact: f64 = (1..=n).map(|k| k as f64).product::<f64>().max(1.0);
118        let c_n = alpha_n * (prefactor / n_fact.sqrt());
119
120        for (i, &x) in x_grid.iter().enumerate() {
121            let phi_n = super::harmonic::qho_wavefunction(n as u32, x, omega, mass, hbar);
122            psi[i] += c_n * phi_n;
123        }
124    }
125    psi
126}
127
128/// Compute the Wigner quasi-probability distribution.
129pub fn wigner_function(psi: &[Complex], x_grid: &[f64], p_grid: &[f64], dx: f64, hbar: f64) -> Vec<Vec<f64>> {
130    let nx = x_grid.len();
131    let np = p_grid.len();
132    let n_psi = psi.len();
133    let mut w = vec![vec![0.0; np]; nx];
134
135    for (ix, &x) in x_grid.iter().enumerate() {
136        for (ip, &p) in p_grid.iter().enumerate() {
137            let mut integral = 0.0;
138            // W(x,p) = 1/(pi*hbar) * integral psi*(x-y) psi(x+y) e^(2ipy/hbar) dy
139            let max_y_steps = (n_psi / 2).min(50);
140            let dy = dx;
141            for k in 0..max_y_steps {
142                let y = k as f64 * dy;
143                let x_plus = x + y;
144                let x_minus = x - y;
145
146                // Interpolate psi at x+y and x-y
147                let psi_plus = interpolate_psi(psi, x_grid, x_plus);
148                let psi_minus = interpolate_psi(psi, x_grid, x_minus);
149
150                let phase = Complex::from_polar(1.0, 2.0 * p * y / hbar);
151                let integrand = psi_minus.conj() * psi_plus * phase;
152
153                let weight = if k == 0 { 1.0 } else { 2.0 }; // symmetric integration
154                integral += integrand.re * weight * dy;
155            }
156            w[ix][ip] = integral / (PI * hbar);
157        }
158    }
159    w
160}
161
162fn interpolate_psi(psi: &[Complex], x_grid: &[f64], x: f64) -> Complex {
163    if x_grid.is_empty() {
164        return Complex::zero();
165    }
166    let n = x_grid.len();
167    let x_min = x_grid[0];
168    let dx = if n > 1 { x_grid[1] - x_grid[0] } else { 1.0 };
169    let idx_f = (x - x_min) / dx;
170    if idx_f < 0.0 || idx_f >= (n - 1) as f64 {
171        return Complex::zero();
172    }
173    let idx = idx_f as usize;
174    let t = idx_f - idx as f64;
175    psi[idx] * (1.0 - t) + psi[idx + 1] * t
176}
177
178/// DFT to momentum representation.
179pub fn momentum_space(psi: &[Complex], dx: f64) -> Vec<Complex> {
180    let mut result = dft(psi);
181    let n = result.len();
182    let norm = (dx / (2.0 * PI).sqrt());
183    for c in &mut result {
184        *c = *c * norm;
185    }
186    result
187}
188
189/// Map complex phase to HSV color.
190pub struct PhaseColorMap;
191
192impl PhaseColorMap {
193    /// Map phase angle in [-pi, pi] to RGB using HSV with full saturation and value.
194    pub fn phase_to_rgb(phase: f64) -> (f64, f64, f64) {
195        let hue = (phase + PI) / (2.0 * PI); // 0 to 1
196        let hue = hue.fract();
197        Self::hsv_to_rgb(hue, 1.0, 1.0)
198    }
199
200    pub fn hsv_to_rgb(h: f64, s: f64, v: f64) -> (f64, f64, f64) {
201        let h = h * 6.0;
202        let c = v * s;
203        let x = c * (1.0 - (h % 2.0 - 1.0).abs());
204        let m = v - c;
205        let (r, g, b) = if h < 1.0 {
206            (c, x, 0.0)
207        } else if h < 2.0 {
208            (x, c, 0.0)
209        } else if h < 3.0 {
210            (0.0, c, x)
211        } else if h < 4.0 {
212            (0.0, x, c)
213        } else if h < 5.0 {
214            (x, 0.0, c)
215        } else {
216            (c, 0.0, x)
217        };
218        (r + m, g + m, b + m)
219    }
220}
221
222/// Density matrix for mixed states.
223#[derive(Clone, Debug)]
224pub struct DensityMatrix {
225    pub rho: Vec<Vec<Complex>>,
226}
227
228impl DensityMatrix {
229    pub fn new(rho: Vec<Vec<Complex>>) -> Self {
230        Self { rho }
231    }
232
233    pub fn from_pure_state(psi: &[Complex]) -> Self {
234        let n = psi.len();
235        let mut rho = vec![vec![Complex::zero(); n]; n];
236        for i in 0..n {
237            for j in 0..n {
238                rho[i][j] = psi[i] * psi[j].conj();
239            }
240        }
241        Self { rho }
242    }
243
244    pub fn dim(&self) -> usize {
245        self.rho.len()
246    }
247
248    pub fn trace(&self) -> Complex {
249        let n = self.dim();
250        let mut t = Complex::zero();
251        for i in 0..n {
252            t += self.rho[i][i];
253        }
254        t
255    }
256
257    /// Purity: Tr(rho^2). 1 for pure states, 1/d for maximally mixed.
258    pub fn purity(&self) -> f64 {
259        let n = self.dim();
260        let mut sum = 0.0;
261        for i in 0..n {
262            for j in 0..n {
263                sum += (self.rho[i][j] * self.rho[j][i]).re;
264            }
265        }
266        sum
267    }
268
269    /// Von Neumann entropy: -Tr(rho * ln(rho)).
270    /// Approximated using eigenvalues from diagonalization for 2x2,
271    /// or using purity-based bound for larger systems.
272    pub fn von_neumann_entropy(&self) -> f64 {
273        let n = self.dim();
274        if n == 2 {
275            // Exact for 2x2: eigenvalues from trace and determinant
276            let tr = self.rho[0][0].re + self.rho[1][1].re;
277            let det = (self.rho[0][0] * self.rho[1][1] - self.rho[0][1] * self.rho[1][0]).re;
278            let disc = (tr * tr - 4.0 * det).max(0.0).sqrt();
279            let l1 = ((tr + disc) / 2.0).max(1e-30);
280            let l2 = ((tr - disc) / 2.0).max(1e-30);
281            let mut s = 0.0;
282            if l1 > 1e-20 { s -= l1 * l1.ln(); }
283            if l2 > 1e-20 { s -= l2 * l2.ln(); }
284            return s;
285        }
286        // For larger matrices, use iterative approach to get diagonal elements
287        // as approximation (exact for diagonal density matrices)
288        let mut s = 0.0;
289        for i in 0..n {
290            let p = self.rho[i][i].re;
291            if p > 1e-20 {
292                s -= p * p.ln();
293            }
294        }
295        s
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_gaussian_wavepacket() {
305        let n = 512;
306        let dx = 0.05;
307        let x_grid: Vec<f64> = (0..n).map(|i| -12.8 + i as f64 * dx).collect();
308        let psi = gaussian_wavepacket(0.0, 0.0, 1.0, &x_grid);
309        let norm_sq: f64 = psi.iter().map(|c| c.norm_sq()).sum::<f64>() * dx;
310        assert!((norm_sq - 1.0).abs() < 0.05, "Norm: {}", norm_sq);
311    }
312
313    #[test]
314    fn test_gaussian_width() {
315        let n = 1024;
316        let dx = 0.05;
317        let sigma = 2.0;
318        let x_grid: Vec<f64> = (0..n).map(|i| -25.0 + i as f64 * dx).collect();
319        let psi = gaussian_wavepacket(0.0, 0.0, sigma, &x_grid);
320        // Width in position: should be sigma
321        let norm_sq: f64 = psi.iter().map(|c| c.norm_sq()).sum::<f64>() * dx;
322        let mean_x: f64 = psi.iter().enumerate().map(|(i, c)| c.norm_sq() * x_grid[i]).sum::<f64>() * dx / norm_sq;
323        let var_x: f64 = psi.iter().enumerate().map(|(i, c)| c.norm_sq() * (x_grid[i] - mean_x).powi(2)).sum::<f64>() * dx / norm_sq;
324        let measured_sigma = var_x.sqrt();
325        assert!((measured_sigma - sigma).abs() < 0.3, "Sigma: {}", measured_sigma);
326    }
327
328    #[test]
329    fn test_momentum_space_gaussian_reciprocal_width() {
330        let n = 256;
331        let dx = 0.1;
332        let sigma = 1.0;
333        let x_grid: Vec<f64> = (0..n).map(|i| -12.8 + i as f64 * dx).collect();
334        let psi = gaussian_wavepacket(0.0, 0.0, sigma, &x_grid);
335        let psi_k = momentum_space(&psi, dx);
336        // In momentum space, width should be ~1/(2*sigma)
337        let dk = 2.0 * PI / (n as f64 * dx);
338        let norm_k: f64 = psi_k.iter().map(|c| c.norm_sq()).sum::<f64>() * dk;
339        // Just verify it's finite and nonzero
340        assert!(norm_k > 0.0);
341    }
342
343    #[test]
344    fn test_plane_wave() {
345        let n = 64;
346        let x_grid: Vec<f64> = (0..n).map(|i| i as f64 * 0.1).collect();
347        let psi = plane_wave(1.0, &x_grid);
348        // All amplitudes should be equal
349        let prob: Vec<f64> = psi.iter().map(|c| c.norm_sq()).collect();
350        let expected = 1.0 / n as f64;
351        for p in &prob {
352            assert!((p - expected).abs() < 1e-10);
353        }
354    }
355
356    #[test]
357    fn test_phase_color_map() {
358        let (r, g, b) = PhaseColorMap::phase_to_rgb(0.0);
359        // Phase 0 maps to hue = 0.5 (cyan-ish)
360        assert!(r >= 0.0 && r <= 1.0);
361        assert!(g >= 0.0 && g <= 1.0);
362        assert!(b >= 0.0 && b <= 1.0);
363    }
364
365    #[test]
366    fn test_density_matrix_pure_state() {
367        let psi = vec![
368            Complex::new(1.0 / 2.0_f64.sqrt(), 0.0),
369            Complex::new(1.0 / 2.0_f64.sqrt(), 0.0),
370        ];
371        let dm = DensityMatrix::from_pure_state(&psi);
372        let purity = dm.purity();
373        assert!((purity - 1.0).abs() < 1e-10, "Purity: {}", purity);
374        let entropy = dm.von_neumann_entropy();
375        assert!(entropy.abs() < 0.01, "Entropy: {}", entropy);
376    }
377
378    #[test]
379    fn test_density_matrix_mixed_state() {
380        // Maximally mixed 2x2
381        let rho = vec![
382            vec![Complex::new(0.5, 0.0), Complex::zero()],
383            vec![Complex::zero(), Complex::new(0.5, 0.0)],
384        ];
385        let dm = DensityMatrix::new(rho);
386        let purity = dm.purity();
387        assert!((purity - 0.5).abs() < 1e-10, "Purity: {}", purity);
388        let entropy = dm.von_neumann_entropy();
389        assert!((entropy - 2.0_f64.ln()).abs() < 0.01, "Entropy: {}", entropy);
390    }
391
392    #[test]
393    fn test_renderer_1d() {
394        let n = 64;
395        let dx = 0.1;
396        let x_grid: Vec<f64> = (0..n).map(|i| -3.2 + i as f64 * dx).collect();
397        let psi = gaussian_wavepacket(0.0, 0.0, 1.0, &x_grid);
398        let wf = WaveFunction1D::new(psi, dx, -3.2);
399        let renderer = WaveFunctionRenderer1D::new(40);
400        let result = renderer.render(&wf);
401        assert_eq!(result.len(), 40);
402    }
403
404    #[test]
405    fn test_renderer_2d() {
406        let nx = 16;
407        let ny = 16;
408        let psi: Vec<Vec<Complex>> = (0..nx)
409            .map(|i| {
410                (0..ny)
411                    .map(|j| {
412                        let r2 = (i as f64 - 8.0).powi(2) + (j as f64 - 8.0).powi(2);
413                        Complex::new((-r2 / 4.0).exp(), 0.0)
414                    })
415                    .collect()
416            })
417            .collect();
418        let renderer = WaveFunctionRenderer2D::new(10, 10);
419        let result = renderer.render(&psi);
420        assert_eq!(result.len(), 10);
421        assert_eq!(result[0].len(), 10);
422    }
423
424    #[test]
425    fn test_wigner_function_runs() {
426        let n = 32;
427        let dx = 0.3;
428        let x_grid: Vec<f64> = (0..n).map(|i| -4.8 + i as f64 * dx).collect();
429        let psi = gaussian_wavepacket(0.0, 0.0, 1.0, &x_grid);
430        let p_grid: Vec<f64> = (0..8).map(|i| -2.0 + i as f64 * 0.5).collect();
431        let w = wigner_function(&psi, &x_grid, &p_grid, dx, 1.0);
432        assert_eq!(w.len(), n);
433        assert_eq!(w[0].len(), 8);
434    }
435}