use crate::error::GeomError;
use crate::fractals::Complex;
use crate::special::legendre::spherical_harmonic_real;
use crate::transforms::fft::{fft, ifft};
#[derive(Debug, Clone)]
pub struct Wavefunction1D {
pub psi: Vec<Complex>,
pub dx: f64,
pub x0: f64,
}
fn scale(z: Complex, k: f64) -> Complex {
Complex::new(z.re * k, z.im * k)
}
impl Wavefunction1D {
pub fn new(psi: Vec<Complex>, dx: f64, x0: f64) -> Result<Self, GeomError> {
if psi.is_empty() {
return Err(GeomError::InvalidArgument("a wavefunction needs samples"));
}
if !(dx > 0.0) {
return Err(GeomError::InvalidArgument("the grid spacing must be positive"));
}
Ok(Self { psi, dx, x0 })
}
pub fn gaussian_packet(
centre: f64,
k0: f64,
sigma: f64,
dx: f64,
x0: f64,
n: usize,
) -> Result<Self, GeomError> {
if !(sigma > 0.0) {
return Err(GeomError::InvalidArgument("the packet width must be positive"));
}
let psi: Vec<Complex> = (0..n)
.map(|k| {
let x = x0 + k as f64 * dx;
let gaussian = (-(x - centre) * (x - centre) / (4.0 * sigma * sigma)).exp();
let phase = k0 * x;
Complex::new(gaussian * phase.cos(), gaussian * phase.sin())
})
.collect();
let mut w = Self::new(psi, dx, x0)?;
w.normalize();
Ok(w)
}
pub fn plane_wave(k: f64, dx: f64, x0: f64, n: usize) -> Result<Self, GeomError> {
let psi: Vec<Complex> = (0..n)
.map(|j| {
let phase = k * (x0 + j as f64 * dx);
Complex::new(phase.cos(), phase.sin())
})
.collect();
let mut w = Self::new(psi, dx, x0)?;
w.normalize();
Ok(w)
}
#[must_use]
pub fn len(&self) -> usize {
self.psi.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
false
}
#[must_use]
pub fn x(&self, k: usize) -> f64 {
self.x0 + k as f64 * self.dx
}
#[must_use]
pub fn norm(&self) -> f64 {
(self.psi.iter().map(|z| z.norm_sq()).sum::<f64>() * self.dx).sqrt()
}
pub fn normalize(&mut self) {
let n = self.norm();
if n > 0.0 {
let inverse = 1.0 / n;
for z in &mut self.psi {
*z = scale(*z, inverse);
}
}
}
#[must_use]
pub fn probability_density(&self) -> Vec<f64> {
self.psi.iter().map(|z| z.norm_sq()).collect()
}
#[must_use]
pub fn expectation_x(&self) -> f64 {
let density = self.probability_density();
let total: f64 = density.iter().sum();
if total <= 0.0 {
return 0.0;
}
density.iter().enumerate().map(|(k, p)| p * self.x(k)).sum::<f64>() / total
}
#[must_use]
pub fn variance_x(&self) -> f64 {
let mean = self.expectation_x();
let density = self.probability_density();
let total: f64 = density.iter().sum();
if total <= 0.0 {
return 0.0;
}
density
.iter()
.enumerate()
.map(|(k, p)| p * (self.x(k) - mean) * (self.x(k) - mean))
.sum::<f64>()
/ total
}
#[must_use]
pub fn wavenumbers(&self) -> Vec<f64> {
let n = self.len();
let span = n as f64 * self.dx;
(0..n)
.map(|k| {
let index = if k <= n / 2 { k as f64 } else { k as f64 - n as f64 };
2.0 * std::f64::consts::PI * index / span
})
.collect()
}
pub fn momentum_space(&self) -> Result<Vec<Complex>, GeomError> {
if !self.len().is_power_of_two() {
return Err(GeomError::InvalidArgument("momentum_space needs a power-of-two grid"));
}
Ok(fft(&self.psi))
}
pub fn expectation_k(&self) -> Result<f64, GeomError> {
let spectrum = self.momentum_space()?;
let weights: Vec<f64> = spectrum.iter().map(|z| z.norm_sq()).collect();
let total: f64 = weights.iter().sum();
if total <= 0.0 {
return Ok(0.0);
}
let k = self.wavenumbers();
Ok(weights.iter().zip(&k).map(|(w, ki)| w * ki).sum::<f64>() / total)
}
pub fn variance_k(&self) -> Result<f64, GeomError> {
let spectrum = self.momentum_space()?;
let weights: Vec<f64> = spectrum.iter().map(|z| z.norm_sq()).collect();
let total: f64 = weights.iter().sum();
if total <= 0.0 {
return Ok(0.0);
}
let k = self.wavenumbers();
let mean = self.expectation_k()?;
Ok(weights
.iter()
.zip(&k)
.map(|(w, ki)| w * (ki - mean) * (ki - mean))
.sum::<f64>()
/ total)
}
pub fn uncertainty_product(&self, hbar: f64) -> Result<f64, GeomError> {
Ok(self.variance_x().sqrt() * (hbar * self.variance_k()?.sqrt()))
}
pub fn overlap(&self, other: &Self) -> Result<Complex, GeomError> {
if self.len() != other.len() || (self.dx - other.dx).abs() > 1e-15 {
return Err(GeomError::InvalidArgument("overlap requires the same grid"));
}
let mut acc = Complex::new(0.0, 0.0);
for (a, b) in other.psi.iter().zip(&self.psi) {
acc = acc + a.conjugate() * *b;
}
Ok(scale(acc, self.dx))
}
pub fn energy(&self, v: &[f64], hbar: f64, mass: f64) -> Result<f64, GeomError> {
if v.len() != self.len() {
return Err(GeomError::InvalidArgument("the potential has the wrong length"));
}
if !(mass > 0.0) {
return Err(GeomError::InvalidArgument("the mass must be positive"));
}
let spectrum = self.momentum_space()?;
let k = self.wavenumbers();
let n = self.len() as f64;
let kinetic: f64 = spectrum
.iter()
.zip(&k)
.map(|(z, ki)| z.norm_sq() * hbar * hbar * ki * ki / (2.0 * mass))
.sum::<f64>()
* self.dx
/ n;
let potential: f64 =
self.psi.iter().zip(v).map(|(z, vi)| z.norm_sq() * vi).sum::<f64>() * self.dx;
let weight = self.norm().powi(2);
if weight <= 0.0 {
return Ok(0.0);
}
Ok((kinetic + potential) / weight)
}
pub fn propagate_free(&self, t: f64, hbar: f64, mass: f64) -> Result<Self, GeomError> {
if !(mass > 0.0) {
return Err(GeomError::InvalidArgument("the mass must be positive"));
}
let mut spectrum = self.momentum_space()?;
let k = self.wavenumbers();
for (z, ki) in spectrum.iter_mut().zip(&k) {
let phase = -hbar * ki * ki * t / (2.0 * mass);
*z = *z * Complex::new(phase.cos(), phase.sin());
}
Ok(Self { psi: ifft(&spectrum), dx: self.dx, x0: self.x0 })
}
}
#[must_use]
pub fn hermite_polynomial(n: usize, x: f64) -> f64 {
if n == 0 {
return 1.0;
}
let mut previous = 1.0;
let mut current = 2.0 * x;
for k in 1..n {
let next = 2.0 * x * current - 2.0 * k as f64 * previous;
previous = current;
current = next;
}
current
}
#[must_use]
pub fn laguerre_associated(n: usize, k: f64, x: f64) -> f64 {
if n == 0 {
return 1.0;
}
let mut previous = 1.0;
let mut current = 1.0 + k - x;
for j in 1..n {
let jf = j as f64;
let next = ((2.0 * jf + 1.0 + k - x) * current - (jf + k) * previous) / (jf + 1.0);
previous = current;
current = next;
}
current
}
#[must_use]
pub fn harmonic_oscillator_eigenstate(
n: usize,
x: f64,
mass: f64,
omega: f64,
hbar: f64,
) -> f64 {
assert!(mass > 0.0 && omega > 0.0 && hbar > 0.0, "the oscillator parameters must be positive");
let alpha = mass * omega / hbar;
let xi = alpha.sqrt() * x;
let mut log_norm = 0.25 * (alpha / std::f64::consts::PI).ln() - 0.5 * n as f64 * 2.0f64.ln();
for k in 1..=n {
log_norm -= 0.5 * (k as f64).ln();
}
log_norm.exp() * hermite_polynomial(n, xi) * (-0.5 * xi * xi).exp()
}
#[must_use]
pub fn harmonic_oscillator_energy(n: usize, omega: f64, hbar: f64) -> f64 {
assert!(omega > 0.0 && hbar > 0.0, "the oscillator parameters must be positive");
(n as f64 + 0.5) * hbar * omega
}
#[must_use]
pub fn infinite_well_eigenstate(n: usize, x: f64, l: f64) -> f64 {
assert!(n >= 1, "the well's states are indexed from one");
assert!(l > 0.0, "the well must have a positive width");
if x <= 0.0 || x >= l {
return 0.0;
}
(2.0 / l).sqrt() * (n as f64 * std::f64::consts::PI * x / l).sin()
}
#[must_use]
pub fn infinite_well_energy(n: usize, l: f64, mass: f64, hbar: f64) -> f64 {
assert!(n >= 1, "the well's states are indexed from one");
assert!(l > 0.0 && mass > 0.0 && hbar > 0.0, "the well parameters must be positive");
let k = n as f64 * std::f64::consts::PI / l;
hbar * hbar * k * k / (2.0 * mass)
}
#[must_use]
pub fn hydrogen_radial(n: usize, l: usize, r: f64, a0: f64) -> f64 {
assert!(n >= 1 && l < n, "hydrogen states require 1 <= n and l < n");
assert!(a0 > 0.0, "the Bohr radius must be positive");
let rho = 2.0 * r / (n as f64 * a0);
let mut log_norm = 1.5 * (2.0 / (n as f64 * a0)).ln();
let mut log_ratio = 0.0;
for k in 1..=(n - l - 1) {
log_ratio += (k as f64).ln();
}
for k in 1..=(n + l) {
log_ratio -= (k as f64).ln();
}
log_norm += 0.5 * (log_ratio - (2.0 * n as f64).ln());
log_norm.exp()
* (-rho / 2.0).exp()
* rho.powi(l as i32)
* laguerre_associated(n - l - 1, 2.0 * l as f64 + 1.0, rho)
}
#[must_use]
pub fn hydrogen_energy(n: usize) -> f64 {
assert!(n >= 1, "hydrogen levels are indexed from one");
-13.605_693_122_994 / (n * n) as f64
}
#[must_use]
pub fn hydrogen_orbital_density(
n: usize,
l: usize,
m: i32,
r: f64,
theta: f64,
phi: f64,
a0: f64,
) -> f64 {
assert!(m.unsigned_abs() as usize <= l, "hydrogen orbitals require |m| <= l");
let radial = hydrogen_radial(n, l, r, a0);
let angular = spherical_harmonic_real(l as u32, m, theta, phi);
radial * radial * angular * angular
}
pub fn coherent_state(alpha: Complex, n_max: usize) -> Result<Vec<Complex>, GeomError> {
if n_max == 0 {
return Err(GeomError::InvalidArgument("coherent_state needs a positive truncation"));
}
let magnitude = alpha.norm();
let phase = alpha.arg();
let mut out = Vec::with_capacity(n_max);
let mut log_term = -0.5 * magnitude * magnitude;
for n in 0..n_max {
if n > 0 {
log_term += magnitude.ln() - 0.5 * (n as f64).ln();
}
let weight = log_term.exp();
let angle = phase * n as f64;
out.push(Complex::new(weight * angle.cos(), weight * angle.sin()));
}
Ok(out)
}
pub fn squeezed_state(r: f64, phi: f64, n_max: usize) -> Result<Vec<Complex>, GeomError> {
if n_max == 0 {
return Err(GeomError::InvalidArgument("squeezed_state needs a positive truncation"));
}
let mut out = vec![Complex::new(0.0, 0.0); n_max];
let sech = 1.0 / r.cosh();
let tanh = r.tanh();
let mut log_coefficient = 0.5 * sech.ln();
for k in 0..n_max.div_ceil(2) {
if k > 0 {
let kf = k as f64;
log_coefficient +=
0.5 * ((2.0 * kf - 1.0).ln() + (2.0 * kf).ln()) - kf.ln() - 2.0f64.ln();
if tanh > 0.0 {
log_coefficient += tanh.ln();
} else {
return Ok(out);
}
}
let magnitude = log_coefficient.exp();
let angle = phi * k as f64 + std::f64::consts::PI * k as f64;
out[2 * k] = Complex::new(magnitude * angle.cos(), magnitude * angle.sin());
}
Ok(out)
}
pub fn wigner_function(
psi: &[Complex],
dx: f64,
x0: f64,
x: f64,
p: f64,
hbar: f64,
) -> Result<f64, GeomError> {
if psi.is_empty() || !(dx > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("wigner_function: bad grid"));
}
let n = psi.len();
let centre = (x - x0) / dx;
let reach = centre.min(n as f64 - 1.0 - centre).floor().max(0.0) as usize;
let mut acc = 0.0;
for offset in 0..=reach {
for sign in [1i64, -1] {
if offset == 0 && sign < 0 {
continue;
}
let step = sign * offset as i64;
let plus = centre.round() as i64 + step;
let minus = centre.round() as i64 - step;
if plus < 0 || minus < 0 || plus >= n as i64 || minus >= n as i64 {
continue;
}
let y = step as f64 * dx;
let product = psi[plus as usize].conjugate() * psi[minus as usize];
let angle = 2.0 * p * y / hbar;
acc += product.re * angle.cos() - product.im * angle.sin();
}
}
Ok(acc * dx / (std::f64::consts::PI * hbar))
}
pub fn husimi_q(
psi: &[Complex],
dx: f64,
x0: f64,
x: f64,
p: f64,
sigma: f64,
hbar: f64,
) -> Result<f64, GeomError> {
if psi.is_empty() || !(dx > 0.0) || !(sigma > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("husimi_q: bad grid"));
}
let normalisation = 1.0 / (2.0 * std::f64::consts::PI * sigma * sigma).powf(0.25);
let mut acc = Complex::new(0.0, 0.0);
for (k, z) in psi.iter().enumerate() {
let xk = x0 + k as f64 * dx;
let envelope =
normalisation * (-(xk - x) * (xk - x) / (4.0 * sigma * sigma)).exp();
let angle = -p * xk / hbar;
let coherent = Complex::new(envelope * angle.cos(), envelope * angle.sin());
acc = acc + coherent.conjugate() * *z;
}
let overlap = scale(acc, dx);
Ok(overlap.norm_sq() / (2.0 * std::f64::consts::PI * hbar))
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn integrate(f: impl Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
let h = (b - a) / n as f64;
(0..n).map(|k| f(a + (k as f64 + 0.5) * h)).sum::<f64>() * h
}
#[test]
fn the_hermite_recurrence_reproduces_the_closed_forms_and_the_roots() {
for x in [-2.0f64, -0.5, 0.0, 0.3, 1.7, 4.0] {
assert!(close(hermite_polynomial(0, x), 1.0, 1e-12));
assert!(close(hermite_polynomial(1, x), 2.0 * x, 1e-12));
assert!(close(hermite_polynomial(2, x), 4.0 * x * x - 2.0, 1e-12));
assert!(close(hermite_polynomial(3, x), 8.0 * x * x * x - 12.0 * x, 1e-12));
assert!(
close(
hermite_polynomial(4, x),
16.0 * x.powi(4) - 48.0 * x * x + 12.0,
1e-11
),
"H_4({x}) is {}",
hermite_polynomial(4, x)
);
}
for n in 0..12 {
for x in [0.4f64, 1.1, 2.6] {
let sign = if n % 2 == 0 { 1.0 } else { -1.0 };
assert!(
close(hermite_polynomial(n, -x), sign * hermite_polynomial(n, x), 1e-9),
"parity fails at n = {n}"
);
}
}
for n in 1..=8 {
let reach = 2.0 * (n as f64).sqrt() + 2.0;
let steps = 20_000;
let h = 2.0 * reach / steps as f64;
let mut changes = 0;
let mut previous = hermite_polynomial(n, -reach + 0.5 * h);
for k in 1..steps {
let x = -reach + (k as f64 + 0.5) * h;
let value = hermite_polynomial(n, x);
if previous * value < 0.0 {
changes += 1;
}
previous = value;
}
assert_eq!(changes, n, "H_{n} should have {n} roots");
}
}
#[test]
fn the_laguerre_recurrence_matches_its_closed_forms() {
for x in [0.0f64, 0.5, 2.0, 6.0] {
for k in [0.0f64, 1.0, 3.0] {
assert!(close(laguerre_associated(0, k, x), 1.0, 1e-12));
assert!(close(laguerre_associated(1, k, x), 1.0 + k - x, 1e-12));
let l2 = x * x / 2.0 - (k + 2.0) * x + (k + 1.0) * (k + 2.0) / 2.0;
assert!(
close(laguerre_associated(2, k, x), l2, 1e-11),
"L_2^{k}({x}) is {}, not {l2}",
laguerre_associated(2, k, x)
);
}
}
for n in 0..8usize {
for k in 0..4usize {
let mut expected = 1.0;
for j in 1..=n {
expected *= (n + k - j + 1) as f64 / j as f64;
}
assert!(
close(laguerre_associated(n, k as f64, 0.0), expected, 1e-9),
"L_{n}^{k}(0) is {}, not {expected}",
laguerre_associated(n, k as f64, 0.0)
);
}
}
}
#[test]
fn the_oscillator_eigenstates_are_orthonormal_and_solve_their_own_equation() {
let (mass, omega, hbar) = (1.0f64, 1.0f64, 1.0f64);
for n in 0..6usize {
for m in 0..6usize {
let overlap = integrate(
|x| {
harmonic_oscillator_eigenstate(n, x, mass, omega, hbar)
* harmonic_oscillator_eigenstate(m, x, mass, omega, hbar)
},
-12.0,
12.0,
40_000,
);
let expected = f64::from(n == m);
assert!(
close(overlap, expected, 1e-8),
"<{n}|{m}> is {overlap}, not {expected}"
);
}
}
let h = 1e-4;
for n in 0..5usize {
for &x in &[-1.3f64, 0.35, 2.1] {
let psi = |y: f64| harmonic_oscillator_eigenstate(n, y, mass, omega, hbar);
let second = (psi(x + h) - 2.0 * psi(x) + psi(x - h)) / (h * h);
let applied = -hbar * hbar / (2.0 * mass) * second
+ 0.5 * mass * omega * omega * x * x * psi(x);
let expected = harmonic_oscillator_energy(n, omega, hbar) * psi(x);
assert!(
close(applied, expected, 1e-5 * (1.0 + expected.abs())),
"n = {n} at x = {x}: H psi is {applied}, E psi is {expected}"
);
}
}
assert!(close(harmonic_oscillator_energy(0, 3.0, 2.0), 3.0, 1e-12));
}
#[test]
fn the_infinite_well_states_are_orthonormal_with_the_textbook_spectrum() {
let l = 2.3f64;
for n in 1..=6usize {
for m in 1..=6usize {
let overlap = integrate(
|x| infinite_well_eigenstate(n, x, l) * infinite_well_eigenstate(m, x, l),
0.0,
l,
50_000,
);
assert!(close(overlap, f64::from(n == m), 1e-6), "<{n}|{m}> is {overlap}");
}
let ratio = infinite_well_energy(n, l, 1.0, 1.0) / infinite_well_energy(1, l, 1.0, 1.0);
assert!(close(ratio, (n * n) as f64, 1e-12), "the ratio at n = {n} is {ratio}");
}
assert_eq!(infinite_well_eigenstate(1, -0.1, l), 0.0);
assert_eq!(infinite_well_eigenstate(1, l + 0.1, l), 0.0);
assert_eq!(infinite_well_eigenstate(1, 0.0, l), 0.0);
}
#[test]
fn the_hydrogen_radial_states_are_normalised_and_have_the_right_node_count() {
let a0 = 1.0f64;
for n in 1..=4usize {
for l in 0..n {
let total = integrate(
|r| {
let value = hydrogen_radial(n, l, r, a0);
value * value * r * r
},
0.0,
60.0 * n as f64,
400_000,
);
assert!(close(total, 1.0, 1e-5), "R_{n},{l} integrates to {total}");
let mut nodes = 0;
let steps = 200_000;
let reach = 60.0 * n as f64;
let h = reach / steps as f64;
let mut previous = hydrogen_radial(n, l, 0.5 * h, a0);
for k in 1..steps {
let r = (k as f64 + 0.5) * h;
let value = hydrogen_radial(n, l, r, a0);
if previous * value < 0.0 {
nodes += 1;
}
previous = value;
}
assert_eq!(nodes, n - l - 1, "R_{n},{l} should have {} nodes", n - l - 1);
}
}
let overlap = integrate(
|r| hydrogen_radial(1, 0, r, a0) * hydrogen_radial(2, 0, r, a0) * r * r,
0.0,
80.0,
400_000,
);
assert!(close(overlap, 0.0, 1e-6), "<1s|2s> is {overlap}");
assert!(close(hydrogen_energy(1), -13.605_693_122_994, 1e-9));
for n in 1..=6usize {
assert!(close(hydrogen_energy(n) * (n * n) as f64, hydrogen_energy(1), 1e-9));
}
}
#[test]
fn the_hydrogen_orbitals_of_a_shell_sum_to_a_spherically_symmetric_density() {
let a0 = 1.0f64;
for (n, l) in [(2usize, 1usize), (3, 1), (3, 2), (4, 2)] {
for &(theta, phi) in
&[(0.3f64, 0.9f64), (1.2, 2.6), (2.9, 0.1), (std::f64::consts::FRAC_PI_2, 4.4)]
{
let total: f64 = (-(l as i32)..=(l as i32))
.map(|m| hydrogen_orbital_density(n, l, m, 1.7, theta, phi, a0))
.sum();
let radial = hydrogen_radial(n, l, 1.7, a0);
let expected =
radial * radial * (2 * l + 1) as f64 / (4.0 * std::f64::consts::PI);
assert!(
close(total, expected, 1e-9),
"the {n},{l} shell is not spherical: {total} against {expected}"
);
}
}
for m in -1i32..=1 {
assert!(hydrogen_orbital_density(2, 1, m, 2.0, 0.4, 1.1, a0) >= 0.0);
}
}
fn gaussian_grid(sigma: f64, k0: f64) -> Wavefunction1D {
let n = 2048usize;
let dx = 40.0 / n as f64;
Wavefunction1D::gaussian_packet(0.0, k0, sigma, dx, -20.0, n).unwrap()
}
#[test]
fn a_gaussian_packet_saturates_the_uncertainty_bound_and_nothing_else_does() {
for sigma in [0.4f64, 0.8, 1.5, 3.0] {
let packet = gaussian_grid(sigma, 0.0);
assert!(close(packet.norm(), 1.0, 1e-12), "the packet is not normalised");
assert!(close(packet.variance_x().sqrt(), sigma, 1e-6), "the width is wrong");
let product = packet.uncertainty_product(1.0).unwrap();
assert!(
close(product, 0.5, 1e-6),
"sigma = {sigma}: the product is {product}, not hbar / 2"
);
}
let n = 2048usize;
let dx = 40.0 / n as f64;
let psi: Vec<Complex> = (0..n)
.map(|k| {
let x = -20.0 + k as f64 * dx;
let left = (-(x + 4.0) * (x + 4.0) / 2.0).exp();
let right = (-(x - 4.0) * (x - 4.0) / 2.0).exp();
Complex::new(left + right, 0.0)
})
.collect();
let mut cat = Wavefunction1D::new(psi, dx, -20.0).unwrap();
cat.normalize();
let product = cat.uncertainty_product(1.0).unwrap();
assert!(product > 0.5, "the product is {product}, below the bound");
assert!(product > 2.5, "two separated peaks should be far from minimal: {product}");
}
#[test]
fn the_packet_carries_the_momentum_it_was_given() {
for k0 in [-3.0f64, -0.5, 0.0, 2.0, 5.5] {
let packet = gaussian_grid(1.0, k0);
let mean = packet.expectation_k().unwrap();
assert!(close(mean, k0, 1e-6), "the mean wavenumber is {mean}, not {k0}");
let spread = packet.variance_k().unwrap().sqrt();
assert!(close(spread, 0.5, 1e-6), "the momentum width is {spread}");
let still = gaussian_grid(1.0, 0.0);
for (a, b) in packet.probability_density().iter().zip(still.probability_density()) {
assert!((a - b).abs() < 1e-12, "the boost changed the position density");
}
}
}
#[test]
fn a_free_packet_spreads_at_the_rate_the_closed_form_gives() {
let sigma0 = 1.0f64;
let packet = gaussian_grid(sigma0, 0.0);
for t in [0.0f64, 0.5, 1.0, 2.0, 4.0] {
let moved = packet.propagate_free(t, 1.0, 1.0).unwrap();
assert!(close(moved.norm(), 1.0, 1e-12), "the norm changed to {}", moved.norm());
let expected = (sigma0 * sigma0 + (t / (2.0 * sigma0)).powi(2)).sqrt();
let width = moved.variance_x().sqrt();
assert!(
close(width, expected, 2e-4),
"at t = {t} the width is {width}, not {expected}"
);
}
let packet = gaussian_grid(1.5, 2.0);
let moved = packet.propagate_free(3.0, 1.0, 1.0).unwrap();
assert!(
close(moved.expectation_x(), 6.0, 2e-3),
"the centre is at {}, not 6",
moved.expectation_x()
);
assert!(close(moved.expectation_k().unwrap(), 2.0, 1e-6));
assert!(close(
moved.variance_k().unwrap(),
packet.variance_k().unwrap(),
1e-12
));
}
#[test]
fn the_energy_of_an_eigenstate_is_its_eigenvalue() {
let n_grid = 2048usize;
let dx = 24.0 / n_grid as f64;
let x0 = -12.0;
let v: Vec<f64> = (0..n_grid).map(|k| 0.5 * (x0 + k as f64 * dx).powi(2)).collect();
for n in 0..6usize {
let psi: Vec<Complex> = (0..n_grid)
.map(|k| {
Complex::new(
harmonic_oscillator_eigenstate(n, x0 + k as f64 * dx, 1.0, 1.0, 1.0),
0.0,
)
})
.collect();
let state = Wavefunction1D::new(psi, dx, x0).unwrap();
let energy = state.energy(&v, 1.0, 1.0).unwrap();
let expected = n as f64 + 0.5;
assert!(
close(energy, expected, 1e-6),
"state {n} has energy {energy}, not {expected}"
);
}
let sigma = 1.2f64;
let k0 = 1.7f64;
let packet = gaussian_grid(sigma, k0);
let free = vec![0.0; packet.len()];
let expected = 0.5 * (k0 * k0 + 1.0 / (4.0 * sigma * sigma));
let energy = packet.energy(&free, 1.0, 1.0).unwrap();
assert!(close(energy, expected, 1e-6), "the packet's energy is {energy}, not {expected}");
}
#[test]
fn overlaps_reproduce_orthonormality_on_the_grid() {
let n_grid = 1024usize;
let dx = 20.0 / n_grid as f64;
let x0 = -10.0;
let state = |n: usize| {
let psi: Vec<Complex> = (0..n_grid)
.map(|k| {
Complex::new(
harmonic_oscillator_eigenstate(n, x0 + k as f64 * dx, 1.0, 1.0, 1.0),
0.0,
)
})
.collect();
Wavefunction1D::new(psi, dx, x0).unwrap()
};
for n in 0..5usize {
for m in 0..5usize {
let value = state(n).overlap(&state(m)).unwrap();
assert!(close(value.re, f64::from(n == m), 1e-8), "<{n}|{m}> is {value:?}");
assert!(close(value.im, 0.0, 1e-12));
}
}
let packet = gaussian_grid(1.0, 1.0);
let self_overlap = packet.overlap(&packet).unwrap();
assert!(close(self_overlap.re, 1.0, 1e-10) && close(self_overlap.im, 0.0, 1e-12));
let other = gaussian_grid(1.0, -1.0);
let forward = packet.overlap(&other).unwrap();
let backward = other.overlap(&packet).unwrap();
assert!(close(forward.re, backward.re, 1e-12));
assert!(close(forward.im, -backward.im, 1e-12));
}
#[test]
fn a_coherent_state_has_poisson_photon_statistics() {
for magnitude in [0.5f64, 1.0, 2.5, 4.0] {
let coefficients = coherent_state(Complex::new(magnitude, 0.0), 120).unwrap();
let weights: Vec<f64> = coefficients.iter().map(|z| z.norm_sq()).collect();
let total: f64 = weights.iter().sum();
assert!(close(total, 1.0, 1e-9), "the state has norm {total}");
let mean: f64 = weights.iter().enumerate().map(|(n, w)| n as f64 * w).sum();
let second: f64 =
weights.iter().enumerate().map(|(n, w)| (n * n) as f64 * w).sum();
let variance = second - mean * mean;
let expected = magnitude * magnitude;
assert!(close(mean, expected, 1e-6), "the mean is {mean}, not {expected}");
assert!(
close(variance, expected, 1e-6),
"Poisson requires variance = mean: {variance} against {expected}"
);
for (n, z) in coefficients.iter().enumerate().take(8) {
let mut factorial = 1.0;
for k in 1..=n {
factorial *= k as f64;
}
let predicted = (-expected / 2.0).exp() * magnitude.powi(n as i32)
/ factorial.sqrt();
assert!(close(z.re, predicted, 1e-9), "coefficient {n} is {}", z.re);
}
}
let rotated = coherent_state(Complex::new(0.0, 2.0), 60).unwrap();
let plain = coherent_state(Complex::new(2.0, 0.0), 60).unwrap();
for (a, b) in rotated.iter().zip(&plain) {
assert!(close(a.norm(), b.norm(), 1e-12));
}
assert!(coherent_state(Complex::new(1.0, 0.0), 0).is_err());
}
#[test]
fn a_squeezed_vacuum_occupies_only_the_even_photon_numbers() {
for r in [0.2f64, 0.5, 1.0] {
let coefficients = squeezed_state(r, 0.4, 200).unwrap();
for (n, z) in coefficients.iter().enumerate() {
if n % 2 == 1 {
assert!(z.norm() < 1e-15, "the odd coefficient {n} is {}", z.norm());
}
}
let total: f64 = coefficients.iter().map(|z| z.norm_sq()).sum();
assert!(close(total, 1.0, 1e-6), "at r = {r} the norm is {total}");
let mean: f64 = coefficients
.iter()
.enumerate()
.map(|(n, z)| n as f64 * z.norm_sq())
.sum();
assert!(
close(mean, r.sinh() * r.sinh(), 1e-5),
"at r = {r} the mean is {mean}, not {}",
r.sinh() * r.sinh()
);
}
let none = squeezed_state(0.0, 0.0, 20).unwrap();
assert!(close(none[0].norm(), 1.0, 1e-12));
assert!(none[1..].iter().all(|z| z.norm() < 1e-15));
assert!(squeezed_state(1.0, 0.0, 0).is_err());
}
#[test]
fn the_wigner_marginals_are_the_position_and_momentum_densities() {
let n = 256usize;
let dx = 12.0 / n as f64;
let x0 = -6.0;
let packet = Wavefunction1D::gaussian_packet(0.0, 1.0, 1.0, dx, x0, n).unwrap();
let p_max = 12.0f64;
let p_steps = 800usize;
let dp = 2.0 * p_max / p_steps as f64;
for &index in &[100usize, 128, 150] {
let x = x0 + index as f64 * dx;
let marginal: f64 = (0..p_steps)
.map(|j| {
let p = -p_max + (j as f64 + 0.5) * dp;
wigner_function(&packet.psi, dx, x0, x, p, 1.0).unwrap()
})
.sum::<f64>()
* dp;
let density = packet.psi[index].norm_sq();
assert!(
close(marginal, density, 1e-3 * (1.0 + density)),
"at x = {x} the marginal is {marginal}, the density {density}"
);
}
for &(x, p) in &[(0.0f64, 1.0f64), (1.0, 0.5), (-2.0, 2.0)] {
assert!(
wigner_function(&packet.psi, dx, x0, x, p, 1.0).unwrap() > -1e-6,
"a Gaussian's Wigner function went negative"
);
}
let psi: Vec<Complex> = (0..n)
.map(|k| {
let x = x0 + k as f64 * dx;
Complex::new(
(-(x + 2.0) * (x + 2.0) / 2.0).exp() + (-(x - 2.0) * (x - 2.0) / 2.0).exp(),
0.0,
)
})
.collect();
let mut cat = Wavefunction1D::new(psi, dx, x0).unwrap();
cat.normalize();
let lowest = (0..40)
.map(|j| {
let p = j as f64 * 0.1;
wigner_function(&cat.psi, dx, x0, 0.0, p, 1.0).unwrap()
})
.fold(f64::INFINITY, f64::min);
assert!(lowest < -0.05, "a Schrodinger cat's Wigner function should go negative: {lowest}");
assert!(wigner_function(&[], 1.0, 0.0, 0.0, 0.0, 1.0).is_err());
}
#[test]
fn the_husimi_function_is_a_genuine_probability_density() {
let n = 256usize;
let dx = 12.0 / n as f64;
let x0 = -6.0;
let psi: Vec<Complex> = (0..n)
.map(|k| {
let x = x0 + k as f64 * dx;
Complex::new(
(-(x + 2.0) * (x + 2.0) / 2.0).exp() + (-(x - 2.0) * (x - 2.0) / 2.0).exp(),
0.0,
)
})
.collect();
let mut cat = Wavefunction1D::new(psi, dx, x0).unwrap();
cat.normalize();
let mut total = 0.0;
let (dp, p_max) = (0.05f64, 6.0f64);
for i in 0..n {
let x = x0 + i as f64 * dx;
let mut j = -p_max;
while j < p_max {
let q = husimi_q(&cat.psi, dx, x0, x, j + dp / 2.0, 0.5, 1.0).unwrap();
assert!(q >= -1e-12, "the Q function went negative at ({x}, {j}): {q}");
total += q * dx * dp;
j += dp;
}
}
assert!(close(total, 1.0, 1e-3), "the Q function integrates to {total}");
let middle = husimi_q(&cat.psi, dx, x0, 0.0, 0.0, 0.5, 1.0).unwrap();
let lump = husimi_q(&cat.psi, dx, x0, 2.0, 0.0, 0.5, 1.0).unwrap();
assert!(lump > 3.0 * middle, "the Q function does not resolve the lumps");
let s = 0.8f64;
let smooth = 0.6f64;
let packet = Wavefunction1D::gaussian_packet(0.0, 0.0, s, dx, x0, n).unwrap();
let a = 1.0 / (4.0 * smooth * smooth);
let b = 1.0 / (4.0 * s * s);
let c_g = (2.0 * std::f64::consts::PI * smooth * smooth).powf(-0.25);
let c_psi = (2.0 * std::f64::consts::PI * s * s).powf(-0.25);
for x in [-1.5f64, -0.5, 0.0, 0.7, 2.0] {
let overlap = c_g
* c_psi
* (std::f64::consts::PI / (a + b)).sqrt()
* (-a * b * x * x / (a + b)).exp();
let expected = overlap * overlap / (2.0 * std::f64::consts::PI);
let got = husimi_q(&packet.psi, dx, x0, x, 0.0, smooth, 1.0).unwrap();
assert!(
close(got, expected, 1e-6),
"at x = {x} the Q function is {got}, the closed form {expected}"
);
}
assert!(husimi_q(&cat.psi, dx, x0, 0.0, 0.0, 0.0, 1.0).is_err());
}
#[test]
fn the_constructors_refuse_degenerate_input() {
assert!(Wavefunction1D::new(vec![], 1.0, 0.0).is_err());
assert!(Wavefunction1D::new(vec![Complex::new(1.0, 0.0)], 0.0, 0.0).is_err());
assert!(Wavefunction1D::gaussian_packet(0.0, 0.0, 0.0, 0.1, -1.0, 16).is_err());
assert!(Wavefunction1D::plane_wave(1.0, 0.1, 0.0, 0).is_err());
let odd = Wavefunction1D::plane_wave(1.0, 0.1, 0.0, 30).unwrap();
assert!(odd.momentum_space().is_err());
assert!(odd.expectation_k().is_err());
assert!(odd.variance_k().is_err());
assert!(odd.uncertainty_product(1.0).is_err());
assert!(odd.propagate_free(0.1, 1.0, 1.0).is_err());
let good = Wavefunction1D::plane_wave(1.0, 0.1, 0.0, 32).unwrap();
assert!(good.energy(&[0.0; 4], 1.0, 1.0).is_err());
assert!(good.energy(&[0.0; 32], 1.0, 0.0).is_err());
assert!(good.propagate_free(0.1, 1.0, -1.0).is_err());
assert!(good.overlap(&odd).is_err());
assert!(!good.is_empty() && good.len() == 32);
let empty = Wavefunction1D::new(vec![Complex::new(0.0, 0.0); 8], 0.1, 0.0).unwrap();
assert_eq!(empty.norm(), 0.0);
assert_eq!(empty.expectation_x(), 0.0);
assert_eq!(empty.variance_x(), 0.0);
assert_eq!(empty.expectation_k().unwrap(), 0.0);
assert_eq!(empty.variance_k().unwrap(), 0.0);
assert_eq!(empty.energy(&[0.0; 8], 1.0, 1.0).unwrap(), 0.0);
let mut still_empty = empty.clone();
still_empty.normalize();
assert!(still_empty.psi.iter().all(|z| z.norm() == 0.0));
}
#[test]
#[should_panic(expected = "l < n")]
fn hydrogen_rejects_an_impossible_angular_momentum() {
let _ = hydrogen_radial(2, 2, 1.0, 1.0);
}
#[test]
#[should_panic(expected = "indexed from one")]
fn the_well_rejects_a_zeroth_state() {
let _ = infinite_well_eigenstate(0, 0.5, 1.0);
}
}