1use std::f64::consts::PI;
2use super::schrodinger::Complex;
3use glam::Vec2;
4
5pub fn hydrogen_energy(n: u32) -> f64 {
7 if n == 0 { return 0.0; }
8 -13.6 / (n as f64 * n as f64)
9}
10
11pub fn associated_legendre(l: u32, m: i32, x: f64) -> f64 {
14 let m_abs = m.unsigned_abs();
15 if m_abs > l {
16 return 0.0;
17 }
18
19 let mut pmm = 1.0;
21 if m_abs > 0 {
22 let somx2 = ((1.0 - x) * (1.0 + x)).sqrt();
23 let mut fact = 1.0;
24 for _i in 0..m_abs {
25 pmm *= -fact * somx2;
26 fact += 2.0;
27 }
28 }
29
30 if l == m_abs {
31 if m < 0 {
32 return adjust_negative_m(l, m, pmm);
33 }
34 return pmm;
35 }
36
37 let mut pmmp1 = x * (2 * m_abs + 1) as f64 * pmm;
39 if l == m_abs + 1 {
40 if m < 0 {
41 return adjust_negative_m(l, m, pmmp1);
42 }
43 return pmmp1;
44 }
45
46 let mut pll = 0.0;
48 for ll in (m_abs + 2)..=l {
49 pll = (x * (2 * ll - 1) as f64 * pmmp1 - (ll + m_abs - 1) as f64 * pmm) / (ll - m_abs) as f64;
50 pmm = pmmp1;
51 pmmp1 = pll;
52 }
53
54 if m < 0 {
55 adjust_negative_m(l, m, pll)
56 } else {
57 pll
58 }
59}
60
61fn adjust_negative_m(l: u32, m: i32, plm: f64) -> f64 {
62 let m_abs = m.unsigned_abs();
63 let sign = if m_abs % 2 == 0 { 1.0 } else { -1.0 };
64 let num: f64 = (1..=(l - m_abs) as u64).map(|k| k as f64).product::<f64>().max(1.0);
65 let den: f64 = (1..=(l + m_abs) as u64).map(|k| k as f64).product::<f64>().max(1.0);
66 sign * (num / den) * plm
67}
68
69pub fn spherical_harmonic(l: u32, m: i32, theta: f64, phi: f64) -> Complex {
71 let m_abs = m.unsigned_abs();
72 if m_abs > l {
73 return Complex::zero();
74 }
75
76 let num: f64 = (1..=(l - m_abs) as u64).map(|k| k as f64).product::<f64>().max(1.0);
78 let den: f64 = (1..=(l + m_abs) as u64).map(|k| k as f64).product::<f64>().max(1.0);
79 let norm = ((2 * l + 1) as f64 / (4.0 * PI) * num / den).sqrt();
80
81 let plm = associated_legendre(l, m.abs(), theta.cos());
82 let phase = Complex::from_polar(1.0, m as f64 * phi);
83
84 let cs_phase = if m > 0 && m % 2 != 0 { -1.0 } else { 1.0 };
85
86 if m >= 0 {
87 phase * (norm * plm * cs_phase)
88 } else {
89 let sign = if m_abs % 2 == 0 { 1.0 } else { -1.0 };
90 phase * (norm * plm * sign)
91 }
92}
93
94fn factorial(n: u64) -> f64 {
96 (1..=n).map(|k| k as f64).product::<f64>().max(1.0)
97}
98
99fn generalized_laguerre(n: u32, alpha: f64, x: f64) -> f64 {
101 if n == 0 {
102 return 1.0;
103 }
104 if n == 1 {
105 return 1.0 + alpha - x;
106 }
107 let mut l_prev = 1.0;
108 let mut l_curr = 1.0 + alpha - x;
109 for k in 1..n {
110 let kf = k as f64;
111 let l_next = ((2.0 * kf + 1.0 + alpha - x) * l_curr - (kf + alpha) * l_prev) / (kf + 1.0);
112 l_prev = l_curr;
113 l_curr = l_next;
114 }
115 l_curr
116}
117
118pub fn radial_wavefunction(n: u32, l: u32, r: f64) -> f64 {
121 if n == 0 || l >= n {
122 return 0.0;
123 }
124 let a0 = 1.0; let nf = n as f64;
126 let rho = 2.0 * r / (nf * a0);
127
128 let num = factorial((n - l - 1) as u64);
130 let den = 2.0 * nf * factorial((n + l) as u64);
131 let norm = ((2.0 / (nf * a0)).powi(3) * num / den).sqrt();
132
133 let laguerre = generalized_laguerre(n - l - 1, 2.0 * l as f64 + 1.0, rho);
134
135 norm * (-rho / 2.0).exp() * rho.powi(l as i32) * laguerre
136}
137
138pub fn hydrogen_orbital(n: u32, l: u32, m: i32, r: f64, theta: f64, phi: f64) -> Complex {
140 if l >= n || (m.unsigned_abs()) > l {
141 return Complex::zero();
142 }
143 let radial = radial_wavefunction(n, l, r);
144 let ylm = spherical_harmonic(l, m, theta, phi);
145 ylm * radial
146}
147
148pub fn probability_density_3d(n: u32, l: u32, m: i32, r: f64, theta: f64, phi: f64) -> f64 {
150 hydrogen_orbital(n, l, m, r, theta, phi).norm_sq()
151}
152
153pub fn radial_probability(n: u32, l: u32, r: f64) -> f64 {
155 let rnl = radial_wavefunction(n, l, r);
156 r * r * rnl * rnl
157}
158
159pub fn orbital_name(n: u32, l: u32, m: i32) -> String {
161 let l_char = match l {
162 0 => 's',
163 1 => 'p',
164 2 => 'd',
165 3 => 'f',
166 4 => 'g',
167 _ => '?',
168 };
169 format!("{}{}_{}", n, l_char, m)
170}
171
172pub fn render_orbital_slice(
174 n: u32,
175 l: u32,
176 m: i32,
177 plane: SlicePlane,
178 grid_size: usize,
179 extent: f64,
180) -> Vec<(Vec2, f64, Complex)> {
181 let mut result = Vec::with_capacity(grid_size * grid_size);
182 let step = 2.0 * extent / grid_size as f64;
183
184 for iy in 0..grid_size {
185 for ix in 0..grid_size {
186 let u = -extent + ix as f64 * step;
187 let v = -extent + iy as f64 * step;
188 let (r, theta, phi) = match plane {
189 SlicePlane::XY => {
190 let r = (u * u + v * v).sqrt();
191 let theta = PI / 2.0; let phi = v.atan2(u);
193 (r, theta, phi)
194 }
195 SlicePlane::XZ => {
196 let r = (u * u + v * v).sqrt();
197 let theta = if r > 1e-15 { (u / r).acos() } else { 0.0 };
198 let phi = 0.0;
199 (r, theta, phi)
200 }
201 SlicePlane::YZ => {
202 let r = (u * u + v * v).sqrt();
203 let theta = if r > 1e-15 { v.atan2(u) } else { 0.0 };
204 let phi = PI / 2.0;
205 (r, theta, phi)
206 }
207 };
208
209 let psi = hydrogen_orbital(n, l, m, r.max(1e-10), theta, phi);
210 let density = psi.norm_sq();
211 result.push((Vec2::new(u as f32, v as f32), density, psi));
212 }
213 }
214 result
215}
216
217#[derive(Clone, Copy, Debug)]
219pub enum SlicePlane {
220 XY,
221 XZ,
222 YZ,
223}
224
225pub struct HydrogenRenderer {
227 pub grid_size: usize,
228 pub extent: f64,
229}
230
231impl HydrogenRenderer {
232 pub fn new(grid_size: usize, extent: f64) -> Self {
233 Self { grid_size, extent }
234 }
235
236 pub fn render(&self, n: u32, l: u32, m: i32, plane: SlicePlane) -> Vec<Vec<(char, f64, f64, f64)>> {
237 let data = render_orbital_slice(n, l, m, plane, self.grid_size, self.extent);
238 let max_density = data.iter().map(|&(_, d, _)| d).fold(0.0_f64, f64::max);
239 let scale = if max_density > 1e-20 { 1.0 / max_density } else { 1.0 };
240
241 let mut grid = vec![vec![(' ', 0.0, 0.0, 0.0); self.grid_size]; self.grid_size];
242 for (idx, &(_, density, psi)) in data.iter().enumerate() {
243 let ix = idx % self.grid_size;
244 let iy = idx / self.grid_size;
245 let brightness = (density * scale).min(1.0);
246 let phase = psi.arg();
247 let (r, g, b) = super::wavefunction::PhaseColorMap::phase_to_rgb(phase);
248 let ch = if brightness > 0.8 {
249 '@'
250 } else if brightness > 0.5 {
251 '#'
252 } else if brightness > 0.2 {
253 '*'
254 } else if brightness > 0.05 {
255 '.'
256 } else {
257 ' '
258 };
259 grid[iy][ix] = (ch, r * brightness, g * brightness, b * brightness);
260 }
261 grid
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_hydrogen_energy_levels() {
271 assert!((hydrogen_energy(1) - (-13.6)).abs() < 0.01);
272 assert!((hydrogen_energy(2) - (-3.4)).abs() < 0.01);
273 assert!((hydrogen_energy(3) - (-13.6 / 9.0)).abs() < 0.01);
274 }
275
276 #[test]
277 fn test_ground_state_normalization() {
278 let dr = 0.01;
280 let r_max = 30.0;
281 let n_pts = (r_max / dr) as usize;
282 let integral: f64 = (1..n_pts)
283 .map(|i| {
284 let r = i as f64 * dr;
285 radial_probability(1, 0, r) * dr
286 })
287 .sum();
288 assert!(
289 (integral - 1.0).abs() < 0.05,
290 "Ground state radial norm: {}",
291 integral
292 );
293 }
294
295 #[test]
296 fn test_2s_normalization() {
297 let dr = 0.02;
298 let r_max = 50.0;
299 let n_pts = (r_max / dr) as usize;
300 let integral: f64 = (1..n_pts)
301 .map(|i| {
302 let r = i as f64 * dr;
303 radial_probability(2, 0, r) * dr
304 })
305 .sum();
306 assert!(
307 (integral - 1.0).abs() < 0.1,
308 "2s radial norm: {}",
309 integral
310 );
311 }
312
313 #[test]
314 fn test_radial_orthogonality() {
315 let dr = 0.01;
317 let n_pts = 3000;
318 let integral: f64 = (1..n_pts)
319 .map(|i| {
320 let r = i as f64 * dr;
321 let r10 = radial_wavefunction(1, 0, r);
322 let r20 = radial_wavefunction(2, 0, r);
323 r * r * r10 * r20 * dr
324 })
325 .sum();
326 assert!(integral.abs() < 0.1, "<R_10|R_20> = {}", integral);
327 }
328
329 #[test]
330 fn test_spherical_harmonic_y00() {
331 let y00 = spherical_harmonic(0, 0, 0.5, 0.3);
333 let expected = 1.0 / (2.0 * PI.sqrt());
334 assert!((y00.re - expected).abs() < 1e-10);
335 assert!(y00.im.abs() < 1e-10);
336 }
337
338 #[test]
339 fn test_spherical_harmonic_normalization() {
340 let n_theta = 100;
342 let n_phi = 100;
343 let dtheta = PI / n_theta as f64;
344 let dphi = 2.0 * PI / n_phi as f64;
345 let integral: f64 = (0..n_theta)
346 .flat_map(|it| {
347 let theta = (it as f64 + 0.5) * dtheta;
348 (0..n_phi).map(move |ip| {
349 let phi = (ip as f64 + 0.5) * dphi;
350 spherical_harmonic(1, 0, theta, phi).norm_sq() * theta.sin() * dtheta * dphi
351 })
352 })
353 .sum();
354 assert!(
355 (integral - 1.0).abs() < 0.1,
356 "Y_1^0 norm: {}",
357 integral
358 );
359 }
360
361 #[test]
362 fn test_associated_legendre() {
363 assert!((associated_legendre(0, 0, 0.5) - 1.0).abs() < 1e-10);
365 assert!((associated_legendre(1, 0, 0.5) - 0.5).abs() < 1e-10);
367 let p11 = associated_legendre(1, 1, 0.5);
369 let expected = -(1.0 - 0.25_f64).sqrt();
370 assert!((p11 - expected).abs() < 1e-10, "P_1^1(0.5) = {}", p11);
371 }
372
373 #[test]
374 fn test_orbital_name() {
375 assert_eq!(orbital_name(1, 0, 0), "1s_0");
376 assert_eq!(orbital_name(2, 1, 1), "2p_1");
377 assert_eq!(orbital_name(3, 2, 0), "3d_0");
378 }
379
380 #[test]
381 fn test_most_probable_radius_1s() {
382 let dr = 0.01;
384 let n_pts = 1000;
385 let mut max_prob = 0.0;
386 let mut max_r = 0.0;
387 for i in 1..n_pts {
388 let r = i as f64 * dr;
389 let p = radial_probability(1, 0, r);
390 if p > max_prob {
391 max_prob = p;
392 max_r = r;
393 }
394 }
395 assert!((max_r - 1.0).abs() < 0.1, "Most probable r = {}", max_r);
396 }
397
398 #[test]
399 fn test_renderer() {
400 let renderer = HydrogenRenderer::new(10, 5.0);
401 let grid = renderer.render(1, 0, 0, SlicePlane::XZ);
402 assert_eq!(grid.len(), 10);
403 assert_eq!(grid[0].len(), 10);
404 }
405
406 #[test]
407 fn test_render_orbital_slice() {
408 let data = render_orbital_slice(2, 1, 0, SlicePlane::XZ, 8, 5.0);
409 assert_eq!(data.len(), 64);
410 }
411}