Skip to main content

proof_engine/quantum/
double_slit.rs

1use std::f64::consts::PI;
2use super::schrodinger::{Complex, SchrodingerSolver2D};
3
4/// Configuration for a double-slit experiment.
5#[derive(Clone, Debug)]
6pub struct DoubleSlitSetup {
7    pub slit_width: f64,
8    pub slit_separation: f64,
9    pub wavelength: f64,
10    pub screen_distance: f64,
11}
12
13impl DoubleSlitSetup {
14    pub fn new(slit_width: f64, slit_separation: f64, wavelength: f64, screen_distance: f64) -> Self {
15        Self { slit_width, slit_separation, wavelength, screen_distance }
16    }
17}
18
19/// Analytical Fraunhofer double-slit intensity pattern.
20/// I(theta) = I_0 * cos^2(pi*d*sin(theta)/lambda) * sinc^2(pi*a*sin(theta)/lambda)
21pub fn intensity_pattern(setup: &DoubleSlitSetup, screen_positions: &[f64]) -> Vec<f64> {
22    let d = setup.slit_separation;
23    let a = setup.slit_width;
24    let lambda = setup.wavelength;
25    let l = setup.screen_distance;
26
27    screen_positions
28        .iter()
29        .map(|&y| {
30            let sin_theta = y / (y * y + l * l).sqrt();
31
32            // Double-slit interference
33            let phase_d = PI * d * sin_theta / lambda;
34            let interference = phase_d.cos().powi(2);
35
36            // Single-slit diffraction envelope
37            let phase_a = PI * a * sin_theta / lambda;
38            let diffraction = if phase_a.abs() < 1e-12 {
39                1.0
40            } else {
41                (phase_a.sin() / phase_a).powi(2)
42            };
43
44            interference * diffraction
45        })
46        .collect()
47}
48
49/// Single-slit Fraunhofer diffraction pattern.
50pub fn single_slit_pattern(width: f64, wavelength: f64, angles: &[f64]) -> Vec<f64> {
51    angles
52        .iter()
53        .map(|&theta| {
54            let beta = PI * width * theta.sin() / wavelength;
55            if beta.abs() < 1e-12 {
56                1.0
57            } else {
58                (beta.sin() / beta).powi(2)
59            }
60        })
61        .collect()
62}
63
64/// N-slit diffraction pattern.
65pub fn n_slit_pattern(n: usize, width: f64, separation: f64, wavelength: f64, angles: &[f64]) -> Vec<f64> {
66    if n == 0 {
67        return vec![0.0; angles.len()];
68    }
69    angles
70        .iter()
71        .map(|&theta| {
72            let sin_t = theta.sin();
73            // Single slit envelope
74            let beta = PI * width * sin_t / wavelength;
75            let envelope = if beta.abs() < 1e-12 {
76                1.0
77            } else {
78                (beta.sin() / beta).powi(2)
79            };
80
81            // N-slit interference
82            let delta = PI * separation * sin_t / wavelength;
83            let n_delta = n as f64 * delta;
84            let multi_slit = if delta.abs() < 1e-12 {
85                (n * n) as f64
86            } else {
87                (n_delta.sin() / delta.sin()).powi(2)
88            };
89
90            envelope * multi_slit / (n * n) as f64
91        })
92        .collect()
93}
94
95/// 2D wave simulation of double-slit experiment.
96pub struct DoubleSlitSimulation {
97    pub solver: SchrodingerSolver2D,
98    pub slit_mask: Vec<Vec<bool>>,
99}
100
101impl DoubleSlitSimulation {
102    /// Create a simulation with slits at a wall position.
103    pub fn new(
104        nx: usize,
105        ny: usize,
106        dx: f64,
107        dy: f64,
108        dt: f64,
109        wall_x_idx: usize,
110        slit1_y: (usize, usize),
111        slit2_y: (usize, usize),
112        barrier_height: f64,
113    ) -> Self {
114        let mut potential = vec![vec![0.0; ny]; nx];
115        let mut slit_mask = vec![vec![false; ny]; nx];
116
117        // Set up barrier wall with two slits
118        for j in 0..ny {
119            let is_slit = (j >= slit1_y.0 && j <= slit1_y.1)
120                || (j >= slit2_y.0 && j <= slit2_y.1);
121            if !is_slit {
122                potential[wall_x_idx][j] = barrier_height;
123            }
124            slit_mask[wall_x_idx][j] = is_slit;
125        }
126
127        let psi = vec![vec![Complex::zero(); ny]; nx];
128        let solver = SchrodingerSolver2D::new(psi, potential, nx, ny, dx, dy, dt, 1.0, 1.0);
129
130        Self { solver, slit_mask }
131    }
132
133    /// Initialize with a plane wave approaching the slits.
134    pub fn init_plane_wave(&mut self, k: f64) {
135        let nx = self.solver.nx;
136        let ny = self.solver.ny;
137        let dx = self.solver.dx;
138        let sigma = nx as f64 * dx * 0.1;
139
140        for i in 0..nx {
141            for j in 0..ny {
142                let x = i as f64 * dx;
143                let x0 = nx as f64 * dx * 0.2;
144                let gauss = (-((x - x0) * (x - x0)) / (2.0 * sigma * sigma)).exp();
145                let phase = k * x;
146                self.solver.psi[i][j] = Complex::from_polar(gauss * 0.1, phase);
147            }
148        }
149    }
150
151    /// Run the simulation for given steps and return final probability density.
152    pub fn run(&mut self, steps: usize) -> Vec<Vec<f64>> {
153        for _ in 0..steps {
154            self.solver.step_2d();
155        }
156        let nx = self.solver.nx;
157        let ny = self.solver.ny;
158        let mut density = vec![vec![0.0; ny]; nx];
159        for i in 0..nx {
160            for j in 0..ny {
161                density[i][j] = self.solver.psi[i][j].norm_sq();
162            }
163        }
164        density
165    }
166}
167
168/// Which-path measurement: detecting which slit kills interference.
169/// Returns the pattern when detection probability is applied.
170pub fn which_path_measurement(
171    setup: &DoubleSlitSetup,
172    screen_positions: &[f64],
173    detection_prob: f64,
174) -> Vec<f64> {
175    let coherent = intensity_pattern(setup, screen_positions);
176
177    // Incoherent sum (no interference, just two single slits)
178    let d = setup.slit_separation;
179    let a = setup.slit_width;
180    let lambda = setup.wavelength;
181    let l = setup.screen_distance;
182
183    let incoherent: Vec<f64> = screen_positions
184        .iter()
185        .map(|&y| {
186            let sin_theta = y / (y * y + l * l).sqrt();
187            let phase_a = PI * a * sin_theta / lambda;
188            let diffraction = if phase_a.abs() < 1e-12 {
189                1.0
190            } else {
191                (phase_a.sin() / phase_a).powi(2)
192            };
193            diffraction // Two single-slit patterns add incoherently
194        })
195        .collect();
196
197    // Interpolate between coherent and incoherent based on detection probability
198    coherent
199        .iter()
200        .zip(incoherent.iter())
201        .map(|(&c, &i)| (1.0 - detection_prob) * c + detection_prob * i)
202        .collect()
203}
204
205/// Render double-slit: wall with slits, interference pattern, particles.
206pub struct DoubleSlitRenderer {
207    pub width: usize,
208    pub height: usize,
209}
210
211impl DoubleSlitRenderer {
212    pub fn new(width: usize, height: usize) -> Self {
213        Self { width, height }
214    }
215
216    /// Render the setup as ASCII art.
217    pub fn render(
218        &self,
219        setup: &DoubleSlitSetup,
220        particle_counts: Option<&[f64]>,
221    ) -> Vec<Vec<char>> {
222        let mut grid = vec![vec![' '; self.width]; self.height];
223        let wall_col = self.width / 3;
224
225        // Draw wall
226        let slit_center1 = self.height / 2 - (self.height as f64 * setup.slit_separation / (4.0 * setup.screen_distance)) as usize;
227        let slit_center2 = self.height / 2 + (self.height as f64 * setup.slit_separation / (4.0 * setup.screen_distance)) as usize;
228        let slit_half = (self.height as f64 * setup.slit_width / (4.0 * setup.screen_distance)).max(1.0) as usize;
229
230        for row in 0..self.height {
231            let is_slit1 = row >= slit_center1.saturating_sub(slit_half)
232                && row <= (slit_center1 + slit_half).min(self.height - 1);
233            let is_slit2 = row >= slit_center2.saturating_sub(slit_half)
234                && row <= (slit_center2 + slit_half).min(self.height - 1);
235            if is_slit1 || is_slit2 {
236                grid[row][wall_col] = ' ';
237            } else {
238                grid[row][wall_col] = '#';
239            }
240        }
241
242        // Draw interference pattern on screen
243        if let Some(counts) = particle_counts {
244            let screen_col = self.width - 2;
245            let max_count = counts.iter().cloned().fold(0.0_f64, f64::max).max(1e-10);
246            for row in 0..self.height {
247                let idx = (row * counts.len()) / self.height;
248                let idx = idx.min(counts.len() - 1);
249                let intensity = counts[idx] / max_count;
250                let n_dots = (intensity * (self.width / 3) as f64) as usize;
251                for c in 0..n_dots.min(self.width - screen_col) {
252                    grid[row][screen_col - c] = if intensity > 0.7 { '@' } else if intensity > 0.3 { '*' } else { '.' };
253                }
254            }
255        }
256
257        grid
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_fringe_spacing() {
267        // Fringe spacing: dy = lambda * D / d
268        let setup = DoubleSlitSetup::new(0.01, 0.1, 0.001, 1.0);
269        let expected_spacing = setup.wavelength * setup.screen_distance / setup.slit_separation;
270
271        // Find peaks
272        let n = 1000;
273        let y_max = 0.05;
274        let positions: Vec<f64> = (0..n).map(|i| -y_max + 2.0 * y_max * i as f64 / n as f64).collect();
275        let pattern = intensity_pattern(&setup, &positions);
276
277        let mut peaks = Vec::new();
278        for i in 1..n - 1 {
279            if pattern[i] > pattern[i - 1] && pattern[i] > pattern[i + 1] && pattern[i] > 0.5 {
280                peaks.push(positions[i]);
281            }
282        }
283
284        if peaks.len() >= 2 {
285            let measured_spacing = (peaks[1] - peaks[0]).abs();
286            assert!(
287                (measured_spacing - expected_spacing).abs() < expected_spacing * 0.3,
288                "Expected spacing {}, got {}",
289                expected_spacing,
290                measured_spacing
291            );
292        }
293    }
294
295    #[test]
296    fn test_single_slit_envelope() {
297        let pattern = single_slit_pattern(0.1, 0.001, &[0.0, 0.01, 0.02]);
298        // At theta=0, intensity should be 1
299        assert!((pattern[0] - 1.0).abs() < 1e-10);
300        // Intensity decreases away from center
301        assert!(pattern[1] <= pattern[0] + 1e-10);
302    }
303
304    #[test]
305    fn test_n_slit_pattern() {
306        // N=1 should equal single slit
307        let angles = vec![0.0, 0.01, 0.02, 0.05];
308        let single = single_slit_pattern(0.1, 0.001, &angles);
309        let n1 = n_slit_pattern(1, 0.1, 0.5, 0.001, &angles);
310        for (a, b) in single.iter().zip(n1.iter()) {
311            assert!((a - b).abs() < 1e-6, "single: {}, n=1: {}", a, b);
312        }
313    }
314
315    #[test]
316    fn test_center_maximum() {
317        let setup = DoubleSlitSetup::new(0.01, 0.1, 0.001, 1.0);
318        let pattern = intensity_pattern(&setup, &[0.0]);
319        assert!((pattern[0] - 1.0).abs() < 1e-10, "Center should be max");
320    }
321
322    #[test]
323    fn test_which_path_kills_fringes() {
324        let setup = DoubleSlitSetup::new(0.01, 0.1, 0.001, 1.0);
325        let positions: Vec<f64> = (0..100).map(|i| -0.05 + 0.001 * i as f64).collect();
326
327        let coherent = intensity_pattern(&setup, &positions);
328        let detected = which_path_measurement(&setup, &positions, 1.0);
329
330        // Coherent pattern should have more variation (interference fringes)
331        let coherent_var: f64 = coherent.iter().map(|&x| (x - 0.5).powi(2)).sum::<f64>();
332        let detected_var: f64 = detected.iter().map(|&x| (x - 0.5).powi(2)).sum::<f64>();
333        // The coherent pattern should have more variance due to fringes
334        assert!(
335            coherent_var > detected_var * 0.5,
336            "Coherent var {} should be >= detected var {}",
337            coherent_var,
338            detected_var
339        );
340    }
341
342    #[test]
343    fn test_simulation_creation() {
344        let mut sim = DoubleSlitSimulation::new(
345            32, 32, 0.5, 0.5, 0.001, 10,
346            (12, 14), (17, 19), 1000.0,
347        );
348        sim.init_plane_wave(5.0);
349        let density = sim.run(5);
350        assert_eq!(density.len(), 32);
351        assert_eq!(density[0].len(), 32);
352    }
353
354    #[test]
355    fn test_renderer() {
356        let setup = DoubleSlitSetup::new(0.01, 0.1, 0.001, 1.0);
357        let renderer = DoubleSlitRenderer::new(40, 20);
358        let grid = renderer.render(&setup, None);
359        assert_eq!(grid.len(), 20);
360        assert_eq!(grid[0].len(), 40);
361        // Should have some wall characters
362        let wall_count: usize = grid.iter().flat_map(|r| r.iter()).filter(|&&c| c == '#').count();
363        assert!(wall_count > 0);
364    }
365
366    #[test]
367    fn test_n_slit_peaks() {
368        // For N slits, there should be N-2 secondary maxima between principal maxima
369        let n = 4;
370        let angles: Vec<f64> = (0..1000).map(|i| -0.1 + 0.0002 * i as f64).collect();
371        let pattern = n_slit_pattern(n, 0.01, 0.1, 0.001, &angles);
372        // Central peak should be 1
373        let center_idx = 500;
374        assert!((pattern[center_idx] - 1.0).abs() < 0.1);
375    }
376}