use crate::error::GeomError;
use crate::fractals::Complex;
use crate::linalg::matrix::Matrix;
use crate::quantum::wavefunction::{harmonic_oscillator_eigenstate, Wavefunction1D};
use crate::transforms::fft::{fft, ifft};
fn scale(z: Complex, k: f64) -> Complex {
Complex::new(z.re * k, z.im * k)
}
fn cis(theta: f64) -> Complex {
Complex::new(theta.cos(), theta.sin())
}
pub fn tise_solve_fd(
v: &[f64],
dx: f64,
mass: f64,
hbar: f64,
n_states: usize,
) -> Result<(Vec<f64>, Vec<Vec<f64>>), GeomError> {
let n = v.len();
if n < 3 {
return Err(GeomError::InvalidArgument("tise_solve_fd needs at least three points"));
}
if !(dx > 0.0) || !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("tise_solve_fd requires positive parameters"));
}
if n_states == 0 || n_states > n {
return Err(GeomError::InvalidArgument("tise_solve_fd: bad state count"));
}
let kinetic = hbar * hbar / (2.0 * mass * dx * dx);
let diag: Vec<f64> = v.iter().map(|vi| 2.0 * kinetic + vi).collect();
let off = vec![-kinetic; n - 1];
let values = lowest_eigenvalues(&diag, &off, n_states);
let mut states = Vec::with_capacity(n_states);
for (i, &lambda) in values.iter().enumerate() {
let mut column = inverse_iteration(&diag, &off, lambda, &states[..i])
.ok_or(GeomError::Degenerate("inverse iteration failed to separate a state"))?;
let norm = (column.iter().map(|c| c * c).sum::<f64>() * dx).sqrt();
if norm > 0.0 {
for c in &mut column {
*c /= norm;
}
}
if let Some(&first) = column.iter().find(|c| c.abs() > 1e-9) {
if first < 0.0 {
for c in &mut column {
*c = -*c;
}
}
}
states.push(column);
}
Ok((values, states))
}
fn sturm_count(diag: &[f64], off_squared: &[f64], sigma: f64) -> usize {
let tiny = 1e-300;
let mut count = 0usize;
let mut pivot = diag[0] - sigma;
for i in 0..diag.len() {
if i > 0 {
pivot = diag[i] - sigma - off_squared[i - 1] / pivot;
}
if pivot.abs() < tiny {
pivot = -tiny;
}
if pivot < 0.0 {
count += 1;
}
}
count
}
fn lowest_eigenvalues(diag: &[f64], off: &[f64], k: usize) -> Vec<f64> {
let n = diag.len();
let off_squared: Vec<f64> = off.iter().map(|b| b * b).collect();
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for i in 0..n {
let radius = (if i > 0 { off[i - 1].abs() } else { 0.0 })
+ (if i + 1 < n { off[i].abs() } else { 0.0 });
lo = lo.min(diag[i] - radius);
hi = hi.max(diag[i] + radius);
}
let span = (hi - lo).max(1.0);
lo -= 1e-9 * span;
hi += 1e-9 * span;
(0..k)
.map(|index| {
let (mut a, mut b) = (lo, hi);
for _ in 0..200 {
let mid = 0.5 * (a + b);
if sturm_count(diag, &off_squared, mid) > index {
b = mid;
} else {
a = mid;
}
if b - a <= 1e-15 * span {
break;
}
}
0.5 * (a + b)
})
.collect()
}
fn tridiagonal_shifted_solve(
diag: &[f64],
off: &[f64],
shift: f64,
rhs: &[f64],
) -> Option<Vec<f64>> {
let n = diag.len();
let mut d: Vec<f64> = diag.iter().map(|a| a - shift).collect();
let mut sub: Vec<f64> = off.to_vec();
let mut sup: Vec<f64> = off.to_vec();
let mut sup2 = vec![0.0f64; n];
let mut r = rhs.to_vec();
for i in 0..n - 1 {
if sub[i].abs() > d[i].abs() {
let (old_d, old_sup) = (d[i], sup[i]);
d[i] = sub[i];
sup[i] = d[i + 1];
sup2[i] = if i + 1 < n - 1 { sup[i + 1] } else { 0.0 };
sub[i] = old_d;
d[i + 1] = old_sup;
if i + 1 < n - 1 {
sup[i + 1] = 0.0;
}
r.swap(i, i + 1);
}
if d[i] == 0.0 {
return None;
}
let factor = sub[i] / d[i];
d[i + 1] -= factor * sup[i];
if i + 1 < n - 1 {
sup[i + 1] -= factor * sup2[i];
}
r[i + 1] -= factor * r[i];
}
if d[n - 1] == 0.0 {
return None;
}
let mut x = vec![0.0f64; n];
x[n - 1] = r[n - 1] / d[n - 1];
if n >= 2 {
x[n - 2] = (r[n - 2] - sup[n - 2] * x[n - 1]) / d[n - 2];
}
for i in (0..n.saturating_sub(2)).rev() {
x[i] = (r[i] - sup[i] * x[i + 1] - sup2[i] * x[i + 2]) / d[i];
}
if x.iter().any(|c| !c.is_finite()) {
return None;
}
Some(x)
}
fn inverse_iteration(
diag: &[f64],
off: &[f64],
lambda: f64,
already: &[Vec<f64>],
) -> Option<Vec<f64>> {
let n = diag.len();
let mut x: Vec<f64> = (0..n)
.map(|k| ((k as f64 * 0.7548776662466927).fract() - 0.5) * 2.0)
.collect();
let magnitude = diag.iter().fold(0.0f64, |acc, a| acc.max(a.abs())).max(1.0);
for attempt in 0..4 {
let shift = lambda + f64::from(attempt) * 1e-11 * magnitude;
let mut converged = false;
for _ in 0..3 {
orthogonalise(&mut x, already);
let normalised = normalise(&mut x);
if !normalised {
return None;
}
let Some(next) = tridiagonal_shifted_solve(diag, off, shift, &x) else {
break;
};
x = next;
converged = true;
}
if converged {
orthogonalise(&mut x, already);
if normalise(&mut x) {
return Some(x);
}
}
x = (0..n).map(|k| ((k as f64 * 0.3819660112501051).fract() - 0.5) * 2.0).collect();
}
None
}
fn orthogonalise(x: &mut [f64], already: &[Vec<f64>]) {
for previous in already {
let projection: f64 = x.iter().zip(previous).map(|(a, b)| a * b).sum();
let square: f64 = previous.iter().map(|b| b * b).sum();
if square > 0.0 {
let factor = projection / square;
for (a, b) in x.iter_mut().zip(previous) {
*a -= factor * b;
}
}
}
}
fn normalise(x: &mut [f64]) -> bool {
let norm = x.iter().map(|c| c * c).sum::<f64>().sqrt();
if !(norm > 0.0) || !norm.is_finite() {
return false;
}
for c in x.iter_mut() {
*c /= norm;
}
true
}
pub fn tise_solve_numerov(
v: &dyn Fn(f64) -> f64,
x_range: (f64, f64),
n: usize,
e_range: (f64, f64),
mass: f64,
hbar: f64,
n_states: usize,
) -> Result<Vec<(f64, Vec<f64>)>, GeomError> {
let (x_lo, x_hi) = x_range;
let (e_lo, e_hi) = e_range;
if n < 5 || !(x_hi > x_lo) || !(e_hi > e_lo) {
return Err(GeomError::InvalidArgument("tise_solve_numerov: bad ranges"));
}
if !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("tise_solve_numerov requires positive constants"));
}
let h = (x_hi - x_lo) / (n - 1) as f64;
let factor = 2.0 * mass / (hbar * hbar);
let sweep = |energy: f64| -> (Vec<f64>, usize) {
let g: Vec<f64> = (0..n).map(|k| factor * (energy - v(x_lo + k as f64 * h))).collect();
let mut y = vec![0.0; n];
y[0] = 0.0;
y[1] = 1e-8;
let c = h * h / 12.0;
for k in 1..n - 1 {
let numerator = 2.0 * (1.0 - 5.0 * c * g[k]) * y[k] - (1.0 + c * g[k - 1]) * y[k - 1];
y[k + 1] = numerator / (1.0 + c * g[k + 1]);
}
let nodes = (1..n - 1).filter(|&k| y[k] * y[k + 1] < 0.0).count();
(y, nodes)
};
let mut found = Vec::new();
for level in 0..n_states {
let (mut lo, mut hi) = (e_lo, e_hi);
let (_, nodes_hi) = sweep(hi);
if nodes_hi <= level {
break;
}
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
let (_, nodes) = sweep(mid);
if nodes > level {
hi = mid;
} else {
lo = mid;
}
if hi - lo < 1e-13 * (1.0 + hi.abs()) {
break;
}
}
let energy = 0.5 * (lo + hi);
let (mut y, _) = sweep(energy);
let norm = (y.iter().map(|c| c * c).sum::<f64>() * h).sqrt();
if norm > 0.0 {
for c in &mut y {
*c /= norm;
}
}
found.push((energy, y));
}
Ok(found)
}
#[derive(Debug, Clone, Copy)]
pub enum Basis {
HarmonicOscillator {
mass: f64,
omega: f64,
},
Box {
length: f64,
},
}
pub fn tise_solve_matrix_basis(
v: &[f64],
dx: f64,
x0: f64,
basis: Basis,
n_basis: usize,
mass: f64,
hbar: f64,
) -> Result<(Vec<f64>, Matrix), GeomError> {
if n_basis == 0 || v.len() < 3 {
return Err(GeomError::InvalidArgument("tise_solve_matrix_basis: bad size"));
}
if !(dx > 0.0) || !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("tise_solve_matrix_basis: bad constants"));
}
let n = v.len();
let x = |k: usize| x0 + k as f64 * dx;
let phi: Vec<Vec<f64>> = (0..n_basis)
.map(|i| match basis {
Basis::HarmonicOscillator { mass: bm, omega } => (0..n)
.map(|k| harmonic_oscillator_eigenstate(i, x(k), bm, omega, hbar))
.collect(),
Basis::Box { length } => (0..n)
.map(|k| {
let xi = x(k);
if xi <= 0.0 || xi >= length {
0.0
} else {
(2.0 / length).sqrt()
* ((i + 1) as f64 * std::f64::consts::PI * xi / length).sin()
}
})
.collect(),
})
.collect();
let mut orthonormal: Vec<Vec<f64>> = Vec::with_capacity(n_basis);
for candidate in &phi {
let mut f = candidate.clone();
for previous in &orthonormal {
let projection: f64 =
f.iter().zip(previous).map(|(a, b)| a * b).sum::<f64>() * dx;
for (a, b) in f.iter_mut().zip(previous) {
*a -= projection * b;
}
}
let norm = (f.iter().map(|a| a * a).sum::<f64>() * dx).sqrt();
if norm > 1e-8 {
for a in &mut f {
*a /= norm;
}
orthonormal.push(f);
}
}
let size = orthonormal.len();
if size == 0 {
return Err(GeomError::Degenerate("the basis vanishes on this grid"));
}
let kinetic = hbar * hbar / (2.0 * mass * dx * dx);
let apply = |f: &[f64]| -> Vec<f64> {
(0..n)
.map(|k| {
let mut acc = (2.0 * kinetic + v[k]) * f[k];
if k > 0 {
acc -= kinetic * f[k - 1];
}
if k + 1 < n {
acc -= kinetic * f[k + 1];
}
acc
})
.collect()
};
let applied: Vec<Vec<f64>> = orthonormal.iter().map(|f| apply(f)).collect();
let mut h = Matrix::zeros(size, size);
for i in 0..size {
for j in i..size {
let element: f64 =
orthonormal[i].iter().zip(&applied[j]).map(|(a, b)| a * b).sum::<f64>() * dx;
h.set(i, j, element);
h.set(j, i, element);
}
}
let decomposition = crate::linalg::eigen::eigen_symmetric(&h, 1e-12, 200)
.map_err(|_| GeomError::Degenerate("the basis eigenproblem failed"))?;
let values: Vec<f64> = decomposition.values.iter().rev().copied().collect();
let vectors = Matrix::from_fn(size, size, |r, c| {
decomposition.vectors.get(r, size - 1 - c)
});
Ok((values, vectors))
}
pub fn tdse_split_operator(
psi: &mut Wavefunction1D,
v: &[f64],
dt: f64,
steps: usize,
mass: f64,
hbar: f64,
) -> Result<(), GeomError> {
if v.len() != psi.len() {
return Err(GeomError::InvalidArgument("the potential has the wrong length"));
}
if !psi.len().is_power_of_two() {
return Err(GeomError::InvalidArgument("split_operator needs a power-of-two grid"));
}
if !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("split_operator requires positive constants"));
}
let k = psi.wavenumbers();
let half: Vec<Complex> = v.iter().map(|vi| cis(-vi * dt / (2.0 * hbar))).collect();
let kinetic: Vec<Complex> =
k.iter().map(|ki| cis(-hbar * ki * ki * dt / (2.0 * mass))).collect();
for _ in 0..steps {
for (z, factor) in psi.psi.iter_mut().zip(&half) {
*z = *z * *factor;
}
let mut spectrum = fft(&psi.psi);
for (z, factor) in spectrum.iter_mut().zip(&kinetic) {
*z = *z * *factor;
}
psi.psi = ifft(&spectrum);
for (z, factor) in psi.psi.iter_mut().zip(&half) {
*z = *z * *factor;
}
}
Ok(())
}
fn complex_thomas(
lower: &[Complex],
diag: &[Complex],
upper: &[Complex],
rhs: &[Complex],
) -> Option<Vec<Complex>> {
let n = diag.len();
let mut c = vec![Complex::new(0.0, 0.0); n];
let mut d = vec![Complex::new(0.0, 0.0); n];
let divide = |a: Complex, b: Complex| -> Option<Complex> {
let denominator = b.norm_sq();
if denominator < 1e-300 {
return None;
}
let numerator = a * b.conjugate();
Some(scale(numerator, 1.0 / denominator))
};
c[0] = divide(upper[0], diag[0])?;
d[0] = divide(rhs[0], diag[0])?;
for i in 1..n {
let pivot = diag[i] - lower[i - 1] * c[i - 1];
if i + 1 < n {
c[i] = divide(upper[i], pivot)?;
}
d[i] = divide(rhs[i] - lower[i - 1] * d[i - 1], pivot)?;
}
let mut x = vec![Complex::new(0.0, 0.0); n];
x[n - 1] = d[n - 1];
for i in (0..n - 1).rev() {
x[i] = d[i] - c[i] * x[i + 1];
}
Some(x)
}
pub fn tdse_crank_nicolson(
psi: &mut Wavefunction1D,
v: &[f64],
dt: f64,
steps: usize,
mass: f64,
hbar: f64,
) -> Result<(), GeomError> {
let n = psi.len();
if v.len() != n {
return Err(GeomError::InvalidArgument("the potential has the wrong length"));
}
if n < 3 || !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("crank_nicolson requires positive constants"));
}
let dx = psi.dx;
let kinetic = hbar * hbar / (2.0 * mass * dx * dx);
let alpha = dt / (2.0 * hbar);
let lower: Vec<Complex> = vec![Complex::new(0.0, alpha * -kinetic); n - 1];
let upper: Vec<Complex> = vec![Complex::new(0.0, alpha * -kinetic); n - 1];
let diag: Vec<Complex> =
v.iter().map(|vi| Complex::new(1.0, alpha * (2.0 * kinetic + vi))).collect();
for _ in 0..steps {
let mut rhs = vec![Complex::new(0.0, 0.0); n];
for i in 0..n {
let mut acc = scale(psi.psi[i], 2.0 * kinetic + v[i]);
if i > 0 {
acc = acc - scale(psi.psi[i - 1], kinetic);
}
if i + 1 < n {
acc = acc - scale(psi.psi[i + 1], kinetic);
}
rhs[i] = psi.psi[i] - Complex::new(0.0, alpha) * acc;
}
psi.psi = complex_thomas(&lower, &diag, &upper, &rhs)
.ok_or(GeomError::Degenerate("the Crank-Nicolson system is singular"))?;
}
Ok(())
}
pub fn absorbing_boundary_cap(
n: usize,
width: usize,
strength: f64,
) -> Result<Vec<f64>, GeomError> {
if width == 0 || 2 * width >= n {
return Err(GeomError::InvalidArgument("the absorbing layers must fit and not meet"));
}
if strength < 0.0 {
return Err(GeomError::InvalidArgument("the absorber strength must be non-negative"));
}
let mut cap = vec![0.0; n];
for k in 0..width {
let depth = (width - k) as f64 / width as f64;
cap[k] = strength * depth * depth;
cap[n - 1 - k] = strength * depth * depth;
}
Ok(cap)
}
pub fn apply_absorber(
psi: &mut Wavefunction1D,
cap: &[f64],
dt: f64,
hbar: f64,
) -> Result<(), GeomError> {
if cap.len() != psi.len() {
return Err(GeomError::InvalidArgument("the absorber has the wrong length"));
}
for (z, c) in psi.psi.iter_mut().zip(cap) {
*z = scale(*z, (-c * dt / hbar).exp());
}
Ok(())
}
pub fn transmission_coefficient(
v: &[f64],
dx: f64,
energy: f64,
mass: f64,
hbar: f64,
) -> Result<f64, GeomError> {
if v.is_empty() || !(dx > 0.0) || !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("transmission_coefficient: bad parameters"));
}
if !(energy > 0.0) {
return Err(GeomError::InvalidArgument("the incident energy must be positive"));
}
let factor = 2.0 * mass / (hbar * hbar);
let wavenumber = |potential: f64| -> Complex {
let squared = factor * (energy - potential);
if squared >= 0.0 {
Complex::new(squared.sqrt(), 0.0)
} else {
Complex::new(0.0, (-squared).sqrt())
}
};
let divide = |a: Complex, b: Complex| -> Complex {
let denominator = b.norm_sq();
if denominator < 1e-300 {
return Complex::new(0.0, 0.0);
}
scale(a * b.conjugate(), 1.0 / denominator)
};
let outside = wavenumber(0.0);
let mut m = [[Complex::new(1.0, 0.0), Complex::new(0.0, 0.0)],
[Complex::new(0.0, 0.0), Complex::new(1.0, 0.0)]];
let multiply = |a: [[Complex; 2]; 2], b: [[Complex; 2]; 2]| -> [[Complex; 2]; 2] {
[
[a[0][0] * b[0][0] + a[0][1] * b[1][0], a[0][0] * b[0][1] + a[0][1] * b[1][1]],
[a[1][0] * b[0][0] + a[1][1] * b[1][0], a[1][0] * b[0][1] + a[1][1] * b[1][1]],
]
};
let mut previous = outside;
for &potential in v {
let k = wavenumber(potential);
let ratio = divide(previous, k);
let half = Complex::new(0.5, 0.0);
let interface = [
[half * (Complex::new(1.0, 0.0) + ratio), half * (Complex::new(1.0, 0.0) - ratio)],
[half * (Complex::new(1.0, 0.0) - ratio), half * (Complex::new(1.0, 0.0) + ratio)],
];
m = multiply(m, interface);
let phase = k * Complex::new(0.0, dx);
let forward = complex_exp(phase);
let backward = complex_exp(scale(phase, -1.0));
let slab = [
[forward, Complex::new(0.0, 0.0)],
[Complex::new(0.0, 0.0), backward],
];
m = multiply(m, slab);
previous = k;
}
let ratio = divide(previous, outside);
let half = Complex::new(0.5, 0.0);
let interface = [
[half * (Complex::new(1.0, 0.0) + ratio), half * (Complex::new(1.0, 0.0) - ratio)],
[half * (Complex::new(1.0, 0.0) - ratio), half * (Complex::new(1.0, 0.0) + ratio)],
];
m = multiply(m, interface);
let denominator = m[0][0].norm_sq();
if denominator < 1e-300 {
return Ok(0.0);
}
Ok(1.0 / denominator)
}
fn complex_exp(z: Complex) -> Complex {
scale(cis(z.im), z.re.exp())
}
pub fn tunneling_rectangular_exact(
v0: f64,
width: f64,
energy: f64,
mass: f64,
hbar: f64,
) -> Result<f64, GeomError> {
if !(width > 0.0) || !(mass > 0.0) || !(hbar > 0.0) || !(energy > 0.0) {
return Err(GeomError::InvalidArgument("tunneling_rectangular_exact: bad parameters"));
}
if v0 == 0.0 {
return Ok(1.0);
}
let factor = 2.0 * mass / (hbar * hbar);
if energy < v0 {
let kappa = (factor * (v0 - energy)).sqrt();
let sinh = (kappa * width).sinh();
Ok(1.0 / (1.0 + v0 * v0 * sinh * sinh / (4.0 * energy * (v0 - energy))))
} else if energy > v0 {
let k = (factor * (energy - v0)).sqrt();
let sin = (k * width).sin();
Ok(1.0 / (1.0 + v0 * v0 * sin * sin / (4.0 * energy * (energy - v0))))
} else {
let k0 = (factor * energy).sqrt();
Ok(1.0 / (1.0 + k0 * k0 * width * width / 4.0))
}
}
pub fn wkb_tunneling(
v: &dyn Fn(f64) -> f64,
energy: f64,
turning_points: (f64, f64),
mass: f64,
hbar: f64,
samples: usize,
) -> Result<f64, GeomError> {
let (a, b) = turning_points;
if !(b > a) || samples == 0 || !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("wkb_tunneling: bad parameters"));
}
let h = (b - a) / samples as f64;
let integral: f64 = (0..samples)
.map(|k| {
let x = a + (k as f64 + 0.5) * h;
let gap = v(x) - energy;
if gap > 0.0 {
(2.0 * mass * gap).sqrt() / hbar
} else {
0.0
}
})
.sum::<f64>()
* h;
Ok((-2.0 * integral).exp())
}
pub fn wkb_quantization(
v: &dyn Fn(f64) -> f64,
n: usize,
e_range: (f64, f64),
x_range: (f64, f64),
mass: f64,
hbar: f64,
samples: usize,
) -> Result<f64, GeomError> {
let (e_lo, e_hi) = e_range;
let (x_lo, x_hi) = x_range;
if !(e_hi > e_lo) || !(x_hi > x_lo) || samples == 0 || !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("wkb_quantization: bad parameters"));
}
let target = (n as f64 + 0.5) * std::f64::consts::PI * hbar;
let action = |energy: f64| -> f64 {
let h = (x_hi - x_lo) / samples as f64;
(0..samples)
.map(|k| {
let x = x_lo + (k as f64 + 0.5) * h;
let gap = energy - v(x);
if gap > 0.0 {
(2.0 * mass * gap).sqrt()
} else {
0.0
}
})
.sum::<f64>()
* h
};
if action(e_lo) > target || action(e_hi) < target {
return Err(GeomError::Degenerate("wkb_quantization: the level is outside the range"));
}
let (mut lo, mut hi) = (e_lo, e_hi);
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if action(mid) < target {
lo = mid;
} else {
hi = mid;
}
if hi - lo < 1e-13 * (1.0 + hi.abs()) {
break;
}
}
Ok(0.5 * (lo + hi))
}
pub fn reflection_step_potential(v0: f64, energy: f64) -> Result<f64, GeomError> {
if !(energy > 0.0) {
return Err(GeomError::InvalidArgument("the incident energy must be positive"));
}
if energy <= v0 {
return Ok(1.0);
}
let k1 = energy.sqrt();
let k2 = (energy - v0).sqrt();
let amplitude = (k1 - k2) / (k1 + k2);
Ok(amplitude * amplitude)
}
pub fn double_well_splitting(
v: &[f64],
dx: f64,
mass: f64,
hbar: f64,
) -> Result<f64, GeomError> {
let (energies, _) = tise_solve_fd(v, dx, mass, hbar, 2)?;
Ok(energies[1] - energies[0])
}
pub fn perturbation_theory_1st(
states: &[Vec<f64>],
perturbation: &[f64],
dx: f64,
) -> Result<Vec<f64>, GeomError> {
if states.is_empty() || !(dx > 0.0) {
return Err(GeomError::InvalidArgument("perturbation_theory_1st: bad input"));
}
if states.iter().any(|s| s.len() != perturbation.len()) {
return Err(GeomError::InvalidArgument("a state has the wrong length"));
}
Ok(states
.iter()
.map(|s| s.iter().zip(perturbation).map(|(c, p)| c * c * p).sum::<f64>() * dx)
.collect())
}
pub fn perturbation_theory_2nd(
states: &[Vec<f64>],
energies: &[f64],
perturbation: &[f64],
dx: f64,
) -> Result<Vec<f64>, GeomError> {
let n = states.len();
if n == 0 || energies.len() != n || !(dx > 0.0) {
return Err(GeomError::InvalidArgument("perturbation_theory_2nd: bad input"));
}
if states.iter().any(|s| s.len() != perturbation.len()) {
return Err(GeomError::InvalidArgument("a state has the wrong length"));
}
let element = |i: usize, j: usize| -> f64 {
states[i]
.iter()
.zip(&states[j])
.zip(perturbation)
.map(|((a, b), p)| a * b * p)
.sum::<f64>()
* dx
};
let mut out = vec![0.0; n];
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
let gap = energies[i] - energies[j];
if gap.abs() < 1e-12 {
return Err(GeomError::Degenerate(
"non-degenerate perturbation theory needs distinct levels",
));
}
let v_ij = element(i, j);
out[i] += v_ij * v_ij / gap;
}
}
Ok(out)
}
pub fn stark_shift_perturbative(
field: f64,
n: usize,
parabolic_difference: i32,
) -> Result<f64, GeomError> {
if n == 0 {
return Err(GeomError::InvalidArgument("hydrogen levels are indexed from one"));
}
if parabolic_difference.unsigned_abs() as usize >= n && n > 1 {
return Err(GeomError::InvalidArgument("the parabolic difference is out of range"));
}
if n == 1 {
return Ok(0.0);
}
Ok(1.5 * n as f64 * f64::from(parabolic_difference) * field)
}
pub fn variational_ground_state(
v: &[f64],
dx: f64,
x0: f64,
trial: &dyn Fn(f64, &[f64]) -> f64,
params0: &[f64],
mass: f64,
hbar: f64,
) -> Result<(f64, Vec<f64>), GeomError> {
if v.len() < 3 || params0.is_empty() || !(dx > 0.0) || !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("variational_ground_state: bad input"));
}
let n = v.len();
let expectation = |params: &[f64]| -> f64 {
let psi: Vec<f64> = (0..n).map(|k| trial(x0 + k as f64 * dx, params)).collect();
let norm: f64 = psi.iter().map(|c| c * c).sum::<f64>() * dx;
if norm <= 0.0 || !norm.is_finite() {
return f64::INFINITY;
}
let mut total = 0.0;
for k in 0..n {
let second = if k == 0 || k + 1 == n {
0.0
} else {
(psi[k + 1] - 2.0 * psi[k] + psi[k - 1]) / (dx * dx)
};
total += psi[k] * (-hbar * hbar / (2.0 * mass) * second + v[k] * psi[k]);
}
let energy = total * dx / norm;
if energy.is_finite() {
energy
} else {
f64::INFINITY
}
};
let best = crate::optimization::nelder_mead(&expectation, params0, 0.2, 1e-12, 20_000);
Ok((expectation(&best), best))
}
pub fn imaginary_time_propagation(
v: &[f64],
dx: f64,
dtau: f64,
steps: usize,
mass: f64,
hbar: f64,
) -> Result<(f64, Vec<f64>), GeomError> {
let n = v.len();
if n < 3 || !(dx > 0.0) || !(dtau > 0.0) || !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("imaginary_time_propagation: bad input"));
}
let mut psi: Vec<f64> = (0..n)
.map(|k| {
let t = k as f64 / (n - 1) as f64;
(std::f64::consts::PI * t).sin() * (1.0 + 0.3 * (3.0 * t).cos())
})
.collect();
let kinetic = hbar * hbar / (2.0 * mass * dx * dx);
let normalise = |psi: &mut Vec<f64>| {
let norm = (psi.iter().map(|c| c * c).sum::<f64>() * dx).sqrt();
if norm > 0.0 {
for c in psi.iter_mut() {
*c /= norm;
}
}
};
normalise(&mut psi);
for _ in 0..steps {
let previous = psi.clone();
for k in 0..n {
let mut applied = (2.0 * kinetic + v[k]) * previous[k];
if k > 0 {
applied -= kinetic * previous[k - 1];
}
if k + 1 < n {
applied -= kinetic * previous[k + 1];
}
psi[k] = previous[k] - dtau * applied / hbar;
}
normalise(&mut psi);
}
let mut total = 0.0;
for k in 0..n {
let mut applied = (2.0 * kinetic + v[k]) * psi[k];
if k > 0 {
applied -= kinetic * psi[k - 1];
}
if k + 1 < n {
applied -= kinetic * psi[k + 1];
}
total += psi[k] * applied;
}
Ok((total * dx, psi))
}
pub fn ehrenfest_check(
snapshots: &[Wavefunction1D],
v: &[f64],
dt: f64,
hbar: f64,
mass: f64,
) -> Result<f64, GeomError> {
if snapshots.len() < 3 || !(dt > 0.0) || !(mass > 0.0) {
return Err(GeomError::InvalidArgument("ehrenfest_check needs a trajectory"));
}
let n = snapshots[0].len();
if v.len() != n {
return Err(GeomError::InvalidArgument("the potential has the wrong length"));
}
let dx = snapshots[0].dx;
let force: Vec<f64> = (0..n)
.map(|k| {
if k == 0 || k + 1 == n {
0.0
} else {
-(v[k + 1] - v[k - 1]) / (2.0 * dx)
}
})
.collect();
let mut worst: f64 = 0.0;
for i in 1..snapshots.len() - 1 {
let before = hbar * snapshots[i - 1].expectation_k()?;
let after = hbar * snapshots[i + 1].expectation_k()?;
let rate = (after - before) / (2.0 * dt);
let density = snapshots[i].probability_density();
let weight: f64 = density.iter().sum::<f64>() * dx;
if weight <= 0.0 {
continue;
}
let expected: f64 = (1..n - 1).map(|k| density[k] * force[k]).sum::<f64>() * dx / weight;
worst = worst.max((rate - expected).abs());
}
let _ = mass;
Ok(worst)
}
pub fn wavepacket_scattering(
v: &[f64],
dx: f64,
x0: f64,
barrier_centre: f64,
k0: f64,
sigma: f64,
start: f64,
dt: f64,
steps: usize,
mass: f64,
hbar: f64,
) -> Result<(f64, f64), GeomError> {
let n = v.len();
if !n.is_power_of_two() || n < 8 {
return Err(GeomError::InvalidArgument("wavepacket_scattering needs a power-of-two grid"));
}
let mut psi = Wavefunction1D::gaussian_packet(start, k0, sigma, dx, x0, n)?;
tdse_split_operator(&mut psi, v, dt, steps, mass, hbar)?;
let density = psi.probability_density();
let total: f64 = density.iter().sum();
if total <= 0.0 {
return Ok((0.0, 0.0));
}
let split = ((barrier_centre - x0) / dx).round().clamp(0.0, (n - 1) as f64) as usize;
let reflected: f64 = density[..split].iter().sum::<f64>() / total;
let transmitted: f64 = density[split..].iter().sum::<f64>() / total;
Ok((transmitted, reflected))
}
pub fn gross_pitaevskii_1d(
psi: &mut Wavefunction1D,
v: &[f64],
g: f64,
dt: f64,
steps: usize,
mass: f64,
hbar: f64,
) -> Result<(), GeomError> {
let n = psi.len();
if v.len() != n {
return Err(GeomError::InvalidArgument("the potential has the wrong length"));
}
if !n.is_power_of_two() || !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("gross_pitaevskii_1d: bad grid"));
}
let k = psi.wavenumbers();
let kinetic: Vec<Complex> =
k.iter().map(|ki| cis(-hbar * ki * ki * dt / (2.0 * mass))).collect();
for _ in 0..steps {
for (z, vi) in psi.psi.iter_mut().zip(v) {
let local = vi + g * z.norm_sq();
*z = *z * cis(-local * dt / (2.0 * hbar));
}
let mut spectrum = fft(&psi.psi);
for (z, factor) in spectrum.iter_mut().zip(&kinetic) {
*z = *z * *factor;
}
psi.psi = ifft(&spectrum);
for (z, vi) in psi.psi.iter_mut().zip(v) {
let local = vi + g * z.norm_sq();
*z = *z * cis(-local * dt / (2.0 * hbar));
}
}
Ok(())
}
#[must_use]
pub fn soliton_bright_exact(
x: f64,
t: f64,
amplitude: f64,
width: f64,
velocity: f64,
mass: f64,
hbar: f64,
) -> Complex {
assert!(amplitude > 0.0 && width > 0.0, "the soliton needs a positive amplitude and width");
let envelope = amplitude / ((x - velocity * t) / width).cosh();
let mu = -hbar * hbar / (2.0 * mass * width * width);
let phase = mass * velocity * x / hbar
- (0.5 * mass * velocity * velocity + mu) * t / hbar;
scale(cis(phase), envelope)
}
#[must_use]
pub fn revival_time(length: f64, mass: f64, hbar: f64) -> f64 {
assert!(length > 0.0 && mass > 0.0 && hbar > 0.0, "revival_time needs positive parameters");
4.0 * mass * length * length / (std::f64::consts::PI * hbar)
}
pub fn quantum_carpet(
length: f64,
coefficients: &[Complex],
times: &[f64],
points: usize,
mass: f64,
hbar: f64,
) -> Result<Vec<Vec<f64>>, GeomError> {
if coefficients.is_empty() || points < 2 || !(length > 0.0) {
return Err(GeomError::InvalidArgument("quantum_carpet: bad input"));
}
if !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("quantum_carpet: bad constants"));
}
let energy = |n: usize| {
let k = (n + 1) as f64 * std::f64::consts::PI / length;
hbar * hbar * k * k / (2.0 * mass)
};
Ok(times
.iter()
.map(|&t| {
(0..points)
.map(|p| {
let x = length * p as f64 / (points - 1) as f64;
let mut acc = Complex::new(0.0, 0.0);
for (n, c) in coefficients.iter().enumerate() {
let shape = (2.0 / length).sqrt()
* ((n + 1) as f64 * std::f64::consts::PI * x / length).sin();
acc = acc + scale(*c * cis(-energy(n) * t / hbar), shape);
}
acc.norm_sq()
})
.collect()
})
.collect())
}
pub fn zeno_survival(t: f64, tau: f64, measurements: usize) -> Result<f64, GeomError> {
if !(tau > 0.0) || measurements == 0 {
return Err(GeomError::InvalidArgument("zeno_survival: bad parameters"));
}
let interval = t / measurements as f64;
let single = (1.0 - (interval / tau).powi(2)).max(0.0);
Ok(single.powi(measurements as i32))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quantum::wavefunction::{
harmonic_oscillator_energy, infinite_well_energy, Wavefunction1D,
};
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn oscillator_grid(n: usize, reach: f64) -> (Vec<f64>, f64, f64) {
let dx = 2.0 * reach / (n - 1) as f64;
let x0 = -reach;
let v = (0..n).map(|k| 0.5 * (x0 + k as f64 * dx).powi(2)).collect();
(v, dx, x0)
}
#[test]
fn finite_differences_recover_the_infinite_well_spectrum_and_converge_from_below() {
let l = 1.0f64;
for n in [400usize, 800, 1600] {
let dx = l / (n + 1) as f64;
let v = vec![0.0; n];
let (energies, states) = tise_solve_fd(&v, dx, 1.0, 1.0, 5).unwrap();
for level in 1..=5usize {
let exact = infinite_well_energy(level, l, 1.0, 1.0);
let got = energies[level - 1];
assert!(got < exact, "level {level} came out at {got}, above the exact {exact}");
assert!(
(got - exact).abs() / exact < 4.0 / (n as f64) * level as f64,
"level {level} at n = {n} is {got} against {exact}"
);
}
for (level, state) in states.iter().enumerate() {
let norm: f64 = state.iter().map(|c| c * c).sum::<f64>() * dx;
assert!(close(norm, 1.0, 1e-9), "state {level} has norm {norm}");
let nodes = (0..state.len() - 1).filter(|&k| state[k] * state[k + 1] < 0.0).count();
assert_eq!(nodes, level, "state {level} should have {level} interior nodes");
}
}
let coarse = {
let dx = l / 401.0;
tise_solve_fd(&vec![0.0; 400], dx, 1.0, 1.0, 1).unwrap().0[0]
};
let fine = {
let dx = l / 801.0;
tise_solve_fd(&vec![0.0; 800], dx, 1.0, 1.0, 1).unwrap().0[0]
};
let exact = infinite_well_energy(1, l, 1.0, 1.0);
let ratio = (coarse - exact).abs() / (fine - exact).abs();
assert!((3.5..4.6).contains(&ratio), "the convergence ratio is {ratio}, not near four");
}
#[test]
fn finite_differences_recover_the_oscillator_ladder() {
let (v, dx, _) = oscillator_grid(2001, 12.0);
let (energies, states) = tise_solve_fd(&v, dx, 1.0, 1.0, 8).unwrap();
for n in 0..8usize {
let exact = harmonic_oscillator_energy(n, 1.0, 1.0);
assert!(
close(energies[n], exact, 1e-3),
"level {n} is {} against {exact}",
energies[n]
);
}
for n in 1..7usize {
let gap = energies[n + 1] - energies[n];
assert!(close(gap, energies[1] - energies[0], 1e-3), "gap {n} is {gap}");
}
let middle = states[0].len() / 2;
for (n, state) in states.iter().enumerate().take(6) {
for offset in [40usize, 120, 300] {
let left = state[middle - offset];
let right = state[middle + offset];
let sign = if n % 2 == 0 { 1.0 } else { -1.0 };
assert!(
(left - sign * right).abs() < 1e-6,
"state {n} has the wrong parity at offset {offset}"
);
}
}
}
#[test]
fn numerov_agrees_with_finite_differences_and_with_the_closed_forms() {
let potential = |x: f64| 0.5 * x * x;
let found =
tise_solve_numerov(&potential, (-10.0, 10.0), 4001, (0.0, 12.0), 1.0, 1.0, 6).unwrap();
assert_eq!(found.len(), 6);
for (n, (energy, state)) in found.iter().enumerate() {
let exact = harmonic_oscillator_energy(n, 1.0, 1.0);
assert!(close(*energy, exact, 1e-6), "level {n} is {energy} against {exact}");
let norm: f64 = state.iter().map(|c| c * c).sum::<f64>() * (20.0 / 4000.0);
assert!(close(norm, 1.0, 1e-6), "state {n} has norm {norm}");
}
let flat = |_: f64| 0.0f64;
let found = tise_solve_numerov(&flat, (0.0, 1.0), 2001, (0.1, 200.0), 1.0, 1.0, 4).unwrap();
for (n, (energy, _)) in found.iter().enumerate() {
let exact = infinite_well_energy(n + 1, 1.0, 1.0, 1.0);
assert!(
(energy - exact).abs() / exact < 1e-8,
"well level {} is {energy} against {exact}",
n + 1
);
}
}
#[test]
fn the_basis_expansion_bounds_the_energies_from_above_as_the_variational_principle_demands() {
let n = 1201usize;
let (v, dx, x0) = oscillator_grid(n, 10.0);
let basis = Basis::HarmonicOscillator { mass: 1.0, omega: 0.7 };
let (reference, _) = tise_solve_fd(&v, dx, 1.0, 1.0, 4).unwrap();
let mut previous = [f64::INFINITY; 4];
for size in [6usize, 10, 16, 24] {
let (energies, coefficients) =
tise_solve_matrix_basis(&v, dx, x0, basis, size, 1.0, 1.0).unwrap();
assert_eq!(coefficients.rows, size);
for level in 0..4usize {
assert!(
energies[level] >= reference[level] - 1e-9,
"level {level} came out at {}, below the operator's own {}",
energies[level],
reference[level]
);
assert!(
energies[level] <= previous[level] + 1e-9,
"level {level} rose from {} to {} as the basis grew",
previous[level],
energies[level]
);
previous[level] = energies[level];
}
}
let (energies, _) =
tise_solve_matrix_basis(&v, dx, x0, basis, 30, 1.0, 1.0).unwrap();
for level in 0..4usize {
assert!(
close(energies[level], harmonic_oscillator_energy(level, 1.0, 1.0), 5e-3),
"level {level} is {}",
energies[level]
);
}
let n = 801usize;
let l = 1.0f64;
let dx = l / (n - 1) as f64;
let (energies, _) =
tise_solve_matrix_basis(&vec![0.0; n], dx, 0.0, Basis::Box { length: l }, 5, 1.0, 1.0)
.unwrap();
for level in 1..=4usize {
let exact = infinite_well_energy(level, l, 1.0, 1.0);
assert!(
(energies[level - 1] - exact).abs() / exact < 5e-3,
"box level {level} is {} against {exact}",
energies[level - 1]
);
}
}
#[test]
fn the_split_operator_is_unitary_and_reproduces_free_spreading() {
let n = 1024usize;
let dx = 40.0 / n as f64;
let v = vec![0.0; n];
for dt in [0.001f64, 0.01, 0.1, 1.0] {
let mut psi = Wavefunction1D::gaussian_packet(0.0, 2.0, 1.0, dx, -20.0, n).unwrap();
tdse_split_operator(&mut psi, &v, dt, 20, 1.0, 1.0).unwrap();
assert!(
close(psi.norm(), 1.0, 1e-12),
"at dt = {dt} the norm became {}",
psi.norm()
);
}
let mut stepped = Wavefunction1D::gaussian_packet(0.0, 2.0, 1.0, dx, -20.0, n).unwrap();
let exact = stepped.propagate_free(2.0, 1.0, 1.0).unwrap();
tdse_split_operator(&mut stepped, &v, 0.02, 100, 1.0, 1.0).unwrap();
for (a, b) in stepped.psi.iter().zip(&exact.psi) {
assert!((a.re - b.re).abs() < 1e-10 && (a.im - b.im).abs() < 1e-10);
}
let (harmonic, hdx, hx0) = oscillator_grid(n, 20.0);
let mut psi = Wavefunction1D::gaussian_packet(2.0, 0.0, 1.0, hdx, hx0, n).unwrap();
let initial = psi.energy(&harmonic, 1.0, 1.0).unwrap();
tdse_split_operator(&mut psi, &harmonic, 0.002, 3000, 1.0, 1.0).unwrap();
let final_energy = psi.energy(&harmonic, 1.0, 1.0).unwrap();
assert!(
close(final_energy, initial, 1e-6),
"the energy drifted from {initial} to {final_energy}"
);
assert!(close(psi.norm(), 1.0, 1e-10), "the norm became {}", psi.norm());
}
#[test]
fn a_coherent_state_orbits_the_harmonic_well_without_changing_shape() {
let n = 1024usize;
let reach = 20.0f64;
let (v, dx, x0) = oscillator_grid(n, reach);
let displacement = 3.0f64;
let mut psi = Wavefunction1D::gaussian_packet(
displacement,
0.0,
1.0 / 2.0f64.sqrt(),
dx,
x0,
n,
)
.unwrap();
let width0 = psi.variance_x().sqrt();
let period = 2.0 * std::f64::consts::PI;
let dt = period / 4000.0;
for quarter in 1..=4usize {
tdse_split_operator(&mut psi, &v, dt, 1000, 1.0, 1.0).unwrap();
let expected = displacement * (quarter as f64 * std::f64::consts::PI / 2.0).cos();
assert!(
close(psi.expectation_x(), expected, 5e-3),
"after a quarter {quarter} the centre is at {}, not {expected}",
psi.expectation_x()
);
assert!(
close(psi.variance_x().sqrt(), width0, 5e-4),
"the width changed to {}",
psi.variance_x().sqrt()
);
}
}
#[test]
fn crank_nicolson_is_unitary_and_agrees_with_the_split_operator() {
let n = 1024usize;
let dx = 60.0 / n as f64;
let x0 = -30.0f64;
let v: Vec<f64> = (0..n).map(|k| 0.5 * (x0 + k as f64 * dx).powi(2) * 0.05).collect();
let start = Wavefunction1D::gaussian_packet(-4.0, 1.0, 1.5, dx, x0, n).unwrap();
let mut cn = start.clone();
tdse_crank_nicolson(&mut cn, &v, 0.002, 500, 1.0, 1.0).unwrap();
assert!(close(cn.norm(), 1.0, 1e-10), "Crank-Nicolson lost norm: {}", cn.norm());
let mut split = start.clone();
tdse_split_operator(&mut split, &v, 0.002, 500, 1.0, 1.0).unwrap();
let mut worst: f64 = 0.0;
for (a, b) in cn.psi.iter().zip(&split.psi) {
worst = worst.max((a.re - b.re).abs()).max((a.im - b.im).abs());
}
assert!(worst < 2e-3, "the two propagators differ by {worst}");
let mut brutal = start.clone();
tdse_crank_nicolson(&mut brutal, &v, 5.0, 20, 1.0, 1.0).unwrap();
assert!(
close(brutal.norm(), 1.0, 1e-9),
"at dt = 5 the norm became {}",
brutal.norm()
);
}
#[test]
fn the_absorbing_layer_removes_outgoing_amplitude_without_reflecting_it() {
let n = 1024usize;
let dx = 60.0 / n as f64;
let x0 = -30.0f64;
let v = vec![0.0; n];
let cap = absorbing_boundary_cap(n, 200, 4.0).unwrap();
assert!(cap[0] > 0.0 && cap[n - 1] > 0.0);
assert!(cap[n / 2] == 0.0, "the middle of the grid must be untouched");
assert!(cap[..200].windows(2).all(|w| w[0] >= w[1]), "the profile must rise inward");
let mut psi = Wavefunction1D::gaussian_packet(0.0, 4.0, 1.5, dx, x0, n).unwrap();
let dt = 0.002;
for _ in 0..6000 {
tdse_split_operator(&mut psi, &v, dt, 1, 1.0, 1.0).unwrap();
apply_absorber(&mut psi, &cap, dt, 1.0).unwrap();
}
assert!(psi.norm() < 0.05, "the absorber left a norm of {}", psi.norm());
let interior: f64 =
psi.probability_density()[300..700].iter().sum::<f64>() * dx;
assert!(interior < 1e-4, "a reflection of weight {interior} came back");
assert!(absorbing_boundary_cap(10, 0, 1.0).is_err());
assert!(absorbing_boundary_cap(10, 5, 1.0).is_err());
assert!(absorbing_boundary_cap(10, 2, -1.0).is_err());
assert!(apply_absorber(&mut psi, &[0.0; 4], 0.1, 1.0).is_err());
}
#[test]
fn the_transfer_matrix_reproduces_the_rectangular_barrier_in_closed_form() {
let (v0, width) = (5.0f64, 1.0f64);
for slices in [50usize, 200, 800] {
let dx = width / slices as f64;
let v = vec![v0; slices];
for energy in [0.5f64, 1.0, 2.5, 4.9, 5.5, 8.0, 20.0] {
let numeric = transmission_coefficient(&v, dx, energy, 1.0, 1.0).unwrap();
let exact = tunneling_rectangular_exact(v0, width, energy, 1.0, 1.0).unwrap();
assert!(
close(numeric, exact, 1e-9),
"at E = {energy} with {slices} slices: {numeric} against {exact}"
);
assert!((0.0..=1.0).contains(&numeric), "the probability is {numeric}");
}
}
let mut previous = 1.0;
for width in [0.5f64, 1.0, 1.5, 2.0, 3.0] {
let t = tunneling_rectangular_exact(5.0, width, 1.0, 1.0, 1.0).unwrap();
assert!(t < previous, "transmission rose with width at {width}");
previous = t;
}
let (v0, energy) = (5.0f64, 1.0f64);
let thick = tunneling_rectangular_exact(v0, 4.0, energy, 1.0, 1.0).unwrap();
let twice = tunneling_rectangular_exact(v0, 8.0, energy, 1.0, 1.0).unwrap();
let prefactor = v0 * v0 / (16.0 * energy * (v0 - energy));
assert!(
(twice / (thick * thick) / prefactor - 1.0).abs() < 1e-3,
"the exponential law fails: {} against {prefactor}",
twice / (thick * thick)
);
}
#[test]
fn a_barrier_is_perfectly_transparent_at_its_resonances() {
let (v0, width) = (2.0f64, 3.0f64);
for m in 1..=5usize {
let k = m as f64 * std::f64::consts::PI / width;
let energy = v0 + k * k / 2.0;
let t = tunneling_rectangular_exact(v0, width, energy, 1.0, 1.0).unwrap();
assert!(close(t, 1.0, 1e-9), "resonance {m} transmits {t}");
let k_off = (m as f64 + 0.5) * std::f64::consts::PI / width;
let off = tunneling_rectangular_exact(v0, width, v0 + k_off * k_off / 2.0, 1.0, 1.0)
.unwrap();
assert!(off < 0.999, "between resonances the transmission is {off}");
}
assert!(close(reflection_step_potential(0.0, 1.0).unwrap(), 0.0, 1e-12));
assert!(close(reflection_step_potential(1.0, 0.5).unwrap(), 1.0, 1e-12));
let over = reflection_step_potential(1.0, 2.0).unwrap();
let expected = {
let (k1, k2) = (2.0f64.sqrt(), 1.0f64);
((k1 - k2) / (k1 + k2)).powi(2)
};
assert!(close(over, expected, 1e-12), "the step reflects {over}, not {expected}");
assert!(over > 0.0, "a classical particle would not reflect at all");
assert!(reflection_step_potential(1.0, 0.0).is_err());
}
#[test]
fn wkb_gets_the_exponent_of_a_thick_barrier_and_the_oscillator_spectrum_exactly() {
let v0 = 5.0f64;
let barrier = |x: f64| if (0.0..3.0).contains(&x) { v0 } else { 0.0 };
let energy = 1.0f64;
let approximate = wkb_tunneling(&barrier, energy, (0.0, 3.0), 1.0, 1.0, 20_000).unwrap();
let kappa = (2.0 * (v0 - energy)).sqrt();
assert!(
close(approximate, (-2.0 * kappa * 3.0).exp(), 1e-9),
"the WKB factor is {approximate}"
);
let exact = tunneling_rectangular_exact(v0, 3.0, energy, 1.0, 1.0).unwrap();
let ratio = exact / approximate;
assert!(
(1.0..40.0).contains(&ratio),
"WKB should be right to a factor of order one, got {ratio}"
);
let potential = |x: f64| 0.5 * x * x;
for n in 0..6usize {
let energy =
wkb_quantization(&potential, n, (0.01, 20.0), (-15.0, 15.0), 1.0, 1.0, 20_000)
.unwrap();
let exact = harmonic_oscillator_energy(n, 1.0, 1.0);
assert!(
(energy - exact).abs() / exact < 2e-4,
"level {n} is {energy} against {exact}"
);
}
assert!(wkb_quantization(&potential, 0, (10.0, 20.0), (-15.0, 15.0), 1.0, 1.0, 100).is_err());
assert!(wkb_tunneling(&barrier, 1.0, (3.0, 0.0), 1.0, 1.0, 100).is_err());
}
#[test]
fn a_wavepacket_scatters_at_roughly_the_rate_the_plane_wave_result_predicts() {
let n = 2048usize;
let dx = 120.0 / n as f64;
let x0 = -60.0f64;
let (v0, width) = (2.0f64, 1.0f64);
let v: Vec<f64> = (0..n)
.map(|k| {
let x = x0 + k as f64 * dx;
if x.abs() < width / 2.0 {
v0
} else {
0.0
}
})
.collect();
let sigma = 3.0f64;
for k0 in [1.6f64, 2.2] {
let (transmitted, reflected) = wavepacket_scattering(
&v, dx, x0, 0.0, k0, sigma, -20.0, 0.01, 2500, 1.0, 1.0,
)
.unwrap();
assert!(
close(transmitted + reflected, 1.0, 1e-9),
"probability is not conserved: {transmitted} + {reflected}"
);
let spread = 1.0 / (2.0 * sigma);
let steps = 4000usize;
let (lo, hi) = (k0 - 6.0 * spread, k0 + 6.0 * spread);
let h = (hi - lo) / steps as f64;
let mut weight_total = 0.0;
let mut weighted = 0.0;
for j in 0..steps {
let k = lo + (j as f64 + 0.5) * h;
if k <= 0.0 {
continue;
}
let w = (-(k - k0) * (k - k0) / (2.0 * spread * spread)).exp();
let t = tunneling_rectangular_exact(v0, width, k * k / 2.0, 1.0, 1.0).unwrap();
weight_total += w;
weighted += w * t;
}
let predicted = weighted / weight_total;
assert!(
(transmitted - predicted).abs() < 0.02,
"at k = {k0} the packet transmitted {transmitted} against the averaged {predicted}"
);
}
let k0 = (2.0f64 * (2.0 + std::f64::consts::PI * std::f64::consts::PI / 2.0)).sqrt();
assert!(
close(tunneling_rectangular_exact(v0, width, k0 * k0 / 2.0, 1.0, 1.0).unwrap(), 1.0, 1e-9),
"the chosen momentum is not a resonance"
);
let spread = 1.0 / (2.0 * 0.5);
let steps = 4000usize;
let (lo, hi) = (k0 - 6.0 * spread, k0 + 6.0 * spread);
let h = (hi - lo) / steps as f64;
let (mut weight_total, mut weighted) = (0.0f64, 0.0f64);
for j in 0..steps {
let k = lo + (j as f64 + 0.5) * h;
if k <= 0.0 {
continue;
}
let w = (-(k - k0) * (k - k0) / (2.0 * spread * spread)).exp();
weight_total += w;
weighted += w * tunneling_rectangular_exact(v0, width, k * k / 2.0, 1.0, 1.0).unwrap();
}
let averaged = weighted / weight_total;
let at_mean = tunneling_rectangular_exact(v0, width, k0 * k0 / 2.0, 1.0, 1.0).unwrap();
assert!(
at_mean - averaged > 0.05,
"a spread of momenta must lose the resonance: {averaged} against {at_mean}"
);
}
#[test]
fn a_double_well_splits_its_ground_doublet_by_less_the_higher_the_barrier() {
let n = 2001usize;
let reach = 6.0f64;
let dx = 2.0 * reach / (n - 1) as f64;
let x0 = -reach;
let mut previous = f64::INFINITY;
let mut splittings = Vec::new();
for barrier in [2.0f64, 4.0, 8.0, 16.0] {
let v: Vec<f64> = (0..n)
.map(|k| {
let x = x0 + k as f64 * dx;
barrier * (x * x - 2.0) * (x * x - 2.0) / 4.0
})
.collect();
let splitting = double_well_splitting(&v, dx, 1.0, 1.0).unwrap();
assert!(splitting > 0.0, "the doublet did not split at all");
assert!(
splitting < previous,
"raising the barrier to {barrier} widened the splitting to {splitting}"
);
previous = splitting;
splittings.push(splitting);
let (_, states) = tise_solve_fd(&v, dx, 1.0, 1.0, 2).unwrap();
let middle = n / 2;
for (level, state) in states.iter().enumerate() {
let sign = if level % 2 == 0 { 1.0 } else { -1.0 };
for offset in [200usize, 400, 600] {
assert!(
(state[middle - offset] - sign * state[middle + offset]).abs() < 1e-6,
"state {level} has the wrong parity"
);
}
}
}
for pair in splittings.windows(2) {
assert!(
pair[0] / pair[1] > 2.0,
"doubling the barrier only reduced the splitting from {} to {}",
pair[0],
pair[1]
);
}
assert!(
splittings[2] / splittings[3] > splittings[0] / splittings[1],
"the fall is not accelerating: {splittings:?}"
);
let v: Vec<f64> = (0..n)
.map(|k| {
let x = x0 + k as f64 * dx;
400.0 * (x * x - 2.0) * (x * x - 2.0) / 4.0
})
.collect();
let splitting = double_well_splitting(&v, dx, 1.0, 1.0).unwrap();
assert!(
splitting < 1e-6,
"the doublet should be all but degenerate here, not split by {splitting}"
);
let (_, states) = tise_solve_fd(&v, dx, 1.0, 1.0, 2).unwrap();
let overlap: f64 =
states[0].iter().zip(&states[1]).map(|(a, b)| a * b).sum::<f64>() * dx;
assert!(
overlap.abs() < 1e-6,
"the two nearly degenerate states came back parallel, overlapping by {overlap}"
);
let (energies, _) = tise_solve_fd(&v, dx, 1.0, 1.0, 2).unwrap();
let kinetic = 1.0 / (2.0 * dx * dx);
let middle = n / 2;
for (level, state) in states.iter().enumerate() {
let mut residual: f64 = 0.0;
for k in 0..n {
let mut applied = (2.0 * kinetic + v[k]) * state[k];
if k > 0 {
applied -= kinetic * state[k - 1];
}
if k + 1 < n {
applied -= kinetic * state[k + 1];
}
residual = residual.max((applied - energies[level] * state[k]).abs());
}
assert!(
residual < 1e-6 * (1.0 + energies[level].abs()),
"state {level} leaves a residual of {residual}"
);
assert!(
state[middle].abs() < 1e-6,
"state {level} has amplitude {} at the barrier top",
state[middle]
);
}
assert!(
splittings[0] / splittings[3] > 100.0,
"an eightfold barrier should cost orders of magnitude: {splittings:?}"
);
}
#[test]
fn perturbation_theory_matches_a_directly_solved_shift_for_a_small_perturbation() {
let n = 2001usize;
let reach = 10.0f64;
let dx = 2.0 * reach / (n - 1) as f64;
let x0 = -reach;
let v: Vec<f64> = (0..n).map(|k| 0.5 * (x0 + k as f64 * dx).powi(2)).collect();
let (energies, states) = tise_solve_fd(&v, dx, 1.0, 1.0, 12).unwrap();
let lambda = 0.02f64;
let perturbation: Vec<f64> =
(0..n).map(|k| lambda * (x0 + k as f64 * dx).powi(4)).collect();
let first = perturbation_theory_1st(&states, &perturbation, dx).unwrap();
assert!(close(first[0], 0.75 * lambda, 1e-5), "the first-order shift is {}", first[0]);
assert!(close(first[1], 3.75 * lambda, 1e-4), "the first excited shift is {}", first[1]);
let second = perturbation_theory_2nd(&states, &energies, &perturbation, dx).unwrap();
assert!(second[0] < 0.0, "the ground state must be pushed down at second order");
let perturbed: Vec<f64> = v.iter().zip(&perturbation).map(|(a, b)| a + b).collect();
let (exact, _) = tise_solve_fd(&perturbed, dx, 1.0, 1.0, 3).unwrap();
let true_shift = exact[0] - energies[0];
let one_term = (energies[0] + first[0] - exact[0]).abs();
let two_terms = (energies[0] + first[0] + second[0] - exact[0]).abs();
assert!(
two_terms < one_term,
"second order made it worse: {two_terms} against {one_term}"
);
assert!(
two_terms < 0.02 * true_shift.abs(),
"two terms leave an error of {two_terms} on a shift of {true_shift}"
);
assert!(
perturbation_theory_2nd(&states, &vec![1.0; states.len()], &perturbation, dx).is_err()
);
assert!(perturbation_theory_1st(&[], &perturbation, dx).is_err());
assert!(perturbation_theory_1st(&states, &[0.0; 3], dx).is_err());
}
#[test]
fn the_stark_shift_vanishes_for_the_ground_state_and_grows_with_the_level() {
assert!(close(stark_shift_perturbative(0.01, 1, 0).unwrap(), 0.0, 1e-15));
assert!(close(stark_shift_perturbative(0.01, 2, 1).unwrap(), 0.03, 1e-12));
assert!(close(stark_shift_perturbative(0.01, 2, -1).unwrap(), -0.03, 1e-12));
assert!(close(stark_shift_perturbative(0.01, 2, 0).unwrap(), 0.0, 1e-15));
let n3 = stark_shift_perturbative(0.01, 3, 2).unwrap();
let n2 = stark_shift_perturbative(0.01, 2, 1).unwrap();
assert!(n3 / n2 > 2.9 && n3 / n2 < 3.1, "the ratio is {}", n3 / n2);
assert!(stark_shift_perturbative(0.01, 0, 0).is_err());
assert!(stark_shift_perturbative(0.01, 2, 5).is_err());
}
#[test]
fn the_variational_energy_is_an_upper_bound_that_the_right_trial_state_saturates() {
let n = 1601usize;
let reach = 8.0f64;
let dx = 2.0 * reach / (n - 1) as f64;
let x0 = -reach;
let trial = |x: f64, p: &[f64]| (-p[0].abs() * x * x).exp();
let harmonic: Vec<f64> = (0..n).map(|k| 0.5 * (x0 + k as f64 * dx).powi(2)).collect();
let (energy, params) =
variational_ground_state(&harmonic, dx, x0, &trial, &[0.9], 1.0, 1.0).unwrap();
assert!(close(energy, 0.5, 1e-5), "the variational energy is {energy}");
assert!(close(params[0].abs(), 0.5, 1e-3), "the parameter came out {}", params[0]);
let quartic: Vec<f64> = (0..n).map(|k| 0.25 * (x0 + k as f64 * dx).powi(4)).collect();
let (bound, _) =
variational_ground_state(&quartic, dx, x0, &trial, &[0.7], 1.0, 1.0).unwrap();
let (exact, _) = tise_solve_fd(&quartic, dx, 1.0, 1.0, 1).unwrap();
assert!(
bound >= exact[0] - 1e-6,
"the variational bound {bound} fell below the true energy {}",
exact[0]
);
assert!(bound < exact[0] + 0.02, "the Gaussian should be a decent trial state: {bound}");
assert!(variational_ground_state(&harmonic, dx, x0, &trial, &[], 1.0, 1.0).is_err());
}
#[test]
fn imaginary_time_finds_the_same_ground_state_the_eigensolver_does() {
let n = 201usize;
let reach = 6.0f64;
let dx = 2.0 * reach / (n - 1) as f64;
let x0 = -reach;
let v: Vec<f64> = (0..n).map(|k| 0.5 * (x0 + k as f64 * dx).powi(2)).collect();
let (energy, state) = imaginary_time_propagation(&v, dx, 1e-3, 20_000, 1.0, 1.0).unwrap();
let (exact, states) = tise_solve_fd(&v, dx, 1.0, 1.0, 1).unwrap();
assert!(close(energy, exact[0], 1e-6), "imaginary time gives {energy}, the solver {}", exact[0]);
let overlap: f64 =
state.iter().zip(&states[0]).map(|(a, b)| a * b).sum::<f64>() * dx;
assert!(
close(overlap.abs(), 1.0, 1e-5),
"the two ground states overlap by {overlap}, not one"
);
let norm: f64 = state.iter().map(|c| c * c).sum::<f64>() * dx;
assert!(close(norm, 1.0, 1e-9));
let interior = &state[5..n - 5];
let nodes = (0..interior.len() - 1)
.filter(|&k| interior[k] * interior[k + 1] < 0.0)
.count();
assert_eq!(nodes, 0, "the ground state should have no nodes");
assert!(imaginary_time_propagation(&v, dx, -1.0, 10, 1.0, 1.0).is_err());
}
#[test]
fn ehrenfest_holds_and_the_harmonic_case_is_the_one_that_is_exact() {
let n = 1024usize;
let reach = 20.0f64;
let (harmonic, dx, x0) = oscillator_grid(n, reach);
let dt = 0.005f64;
let mut psi = Wavefunction1D::gaussian_packet(2.0, 0.0, 1.0, dx, x0, n).unwrap();
let mut snapshots = vec![psi.clone()];
for _ in 0..40 {
tdse_split_operator(&mut psi, &harmonic, dt, 1, 1.0, 1.0).unwrap();
snapshots.push(psi.clone());
}
let worst = ehrenfest_check(&snapshots, &harmonic, dt, 1.0, 1.0).unwrap();
assert!(worst < 1e-4, "Ehrenfest fails in a harmonic well by {worst}");
let mut finer = Wavefunction1D::gaussian_packet(2.0, 0.0, 1.0, dx, x0, n).unwrap();
let small = dt / 2.0;
let mut fine_snapshots = vec![finer.clone()];
for _ in 0..40 {
tdse_split_operator(&mut finer, &harmonic, small, 1, 1.0, 1.0).unwrap();
fine_snapshots.push(finer.clone());
}
let refined = ehrenfest_check(&fine_snapshots, &harmonic, small, 1.0, 1.0).unwrap();
let ratio = worst / refined;
assert!(
(3.0..5.0).contains(&ratio),
"the residual fell by {ratio}, not the fourfold of a second-order error"
);
let elapsed = 40.0 * dt;
assert!(
close(psi.expectation_x(), 2.0 * elapsed.cos(), 1e-4),
"the centre is at {}, not {}",
psi.expectation_x(),
2.0 * elapsed.cos()
);
let quartic: Vec<f64> =
(0..n).map(|k| 0.02 * (x0 + k as f64 * dx).powi(4)).collect();
let mut psi = Wavefunction1D::gaussian_packet(2.0, 0.0, 1.0, dx, x0, n).unwrap();
let mut snapshots = vec![psi.clone()];
for _ in 0..40 {
tdse_split_operator(&mut psi, &quartic, dt, 1, 1.0, 1.0).unwrap();
snapshots.push(psi.clone());
}
let worst = ehrenfest_check(&snapshots, &quartic, dt, 1.0, 1.0).unwrap();
assert!(worst < 1e-3, "Ehrenfest fails in a quartic well by {worst}");
let density = snapshots[20].probability_density();
let weight: f64 = density.iter().sum();
let mean_x: f64 =
density.iter().enumerate().map(|(k, p)| p * (x0 + k as f64 * dx)).sum::<f64>() / weight;
let mean_force: f64 = density
.iter()
.enumerate()
.map(|(k, p)| p * -0.08 * (x0 + k as f64 * dx).powi(3))
.sum::<f64>()
/ weight;
let force_at_mean = -0.08 * mean_x.powi(3);
assert!(
(mean_force - force_at_mean).abs() > 1e-3,
"the two forces agree to {}, so the quartic case is not being tested",
(mean_force - force_at_mean).abs()
);
assert!(ehrenfest_check(&snapshots[..2], &quartic, dt, 1.0, 1.0).is_err());
}
#[test]
fn a_bright_soliton_propagates_without_spreading_and_a_free_packet_does_not() {
let n = 2048usize;
let dx = 60.0 / n as f64;
let x0 = -30.0f64;
let v = vec![0.0; n];
let width = 1.5f64;
let amplitude = 1.0 / width;
let g = -1.0f64;
let psi0: Vec<Complex> = (0..n)
.map(|k| soliton_bright_exact(x0 + k as f64 * dx, 0.0, amplitude, width, 0.0, 1.0, 1.0))
.collect();
let mut soliton = Wavefunction1D::new(psi0.clone(), dx, x0).unwrap();
let initial_width = soliton.variance_x().sqrt();
gross_pitaevskii_1d(&mut soliton, &v, g, 0.0025, 2000, 1.0, 1.0).unwrap();
let after = soliton.variance_x().sqrt();
assert!(
close(after, initial_width, 5e-3),
"the soliton spread from {initial_width} to {after}"
);
let mut free = Wavefunction1D::new(psi0, dx, x0).unwrap();
gross_pitaevskii_1d(&mut free, &v, 0.0, 0.0025, 2000, 1.0, 1.0).unwrap();
let spread = free.variance_x().sqrt();
assert!(
spread > initial_width * 1.5,
"without the nonlinearity it should spread: {spread} against {initial_width}"
);
assert!(close(soliton.norm(), free.norm(), 1e-9));
assert!(gross_pitaevskii_1d(&mut free, &[0.0; 3], g, 0.01, 1, 1.0, 1.0).is_err());
}
#[test]
fn a_box_state_revives_exactly_at_the_revival_time() {
let l = 1.0f64;
let revival = revival_time(l, 1.0, 1.0);
let coefficients: Vec<Complex> = (0..8)
.map(|n| Complex::new(1.0 / ((n + 1) as f64), 0.0))
.collect();
let carpet = quantum_carpet(
l,
&coefficients,
&[0.0, revival / 2.0, revival, 0.137 * revival],
201,
1.0,
1.0,
)
.unwrap();
assert_eq!(carpet.len(), 4);
for (a, b) in carpet[0].iter().zip(&carpet[2]) {
assert!((a - b).abs() < 1e-9, "the state did not revive: {a} against {b}");
}
let points = carpet[0].len();
for (k, value) in carpet[1].iter().enumerate() {
let mirrored = carpet[0][points - 1 - k];
assert!(
(value - mirrored).abs() < 1e-9,
"the half revival is not a mirror at point {k}"
);
}
let generic: f64 = carpet[3]
.iter()
.zip(&carpet[0])
.map(|(a, b)| (a - b).abs())
.fold(0.0, f64::max);
assert!(generic > 1e-3, "the state should differ at a generic time: {generic}");
assert!(quantum_carpet(l, &[], &[0.0], 10, 1.0, 1.0).is_err());
assert!(quantum_carpet(l, &coefficients, &[0.0], 1, 1.0, 1.0).is_err());
}
#[test]
fn watching_a_state_stops_it_decaying() {
let (t, tau) = (0.5f64, 1.0f64);
let mut previous = 0.0;
for measurements in [1usize, 2, 5, 20, 100, 1000] {
let survival = zeno_survival(t, tau, measurements).unwrap();
assert!(
survival > previous,
"with {measurements} checks the survival fell to {survival}"
);
assert!((0.0..=1.0).contains(&survival));
previous = survival;
}
assert!(previous > 0.99, "frequent measurement should nearly freeze it: {previous}");
assert!(close(zeno_survival(0.3, 1.0, 1).unwrap(), 1.0 - 0.09, 1e-12));
assert!(zeno_survival(1.0, 0.0, 5).is_err());
assert!(zeno_survival(1.0, 1.0, 0).is_err());
}
#[test]
fn the_solvers_refuse_degenerate_input() {
assert!(tise_solve_fd(&[1.0, 2.0], 0.1, 1.0, 1.0, 1).is_err());
assert!(tise_solve_fd(&[1.0; 5], 0.0, 1.0, 1.0, 1).is_err());
assert!(tise_solve_fd(&[1.0; 5], 0.1, 0.0, 1.0, 1).is_err());
assert!(tise_solve_fd(&[1.0; 5], 0.1, 1.0, 1.0, 0).is_err());
assert!(tise_solve_fd(&[1.0; 5], 0.1, 1.0, 1.0, 9).is_err());
assert!(tise_solve_numerov(&|_| 0.0, (1.0, 0.0), 100, (0.0, 1.0), 1.0, 1.0, 1).is_err());
assert!(tise_solve_numerov(&|_| 0.0, (0.0, 1.0), 3, (0.0, 1.0), 1.0, 1.0, 1).is_err());
assert!(tise_solve_numerov(&|_| 0.0, (0.0, 1.0), 100, (1.0, 0.0), 1.0, 1.0, 1).is_err());
assert!(
tise_solve_matrix_basis(&[1.0; 5], 0.1, 0.0, Basis::Box { length: 1.0 }, 0, 1.0, 1.0)
.is_err()
);
assert!(transmission_coefficient(&[], 0.1, 1.0, 1.0, 1.0).is_err());
assert!(transmission_coefficient(&[1.0], 0.1, 0.0, 1.0, 1.0).is_err());
assert!(tunneling_rectangular_exact(1.0, 0.0, 1.0, 1.0, 1.0).is_err());
assert!(close(tunneling_rectangular_exact(0.0, 2.0, 3.0, 1.0, 1.0).unwrap(), 1.0, 1e-15));
let top = tunneling_rectangular_exact(2.0, 1.0, 2.0, 1.0, 1.0).unwrap();
let just_below = tunneling_rectangular_exact(2.0, 1.0, 2.0 - 1e-7, 1.0, 1.0).unwrap();
let just_above = tunneling_rectangular_exact(2.0, 1.0, 2.0 + 1e-7, 1.0, 1.0).unwrap();
assert!(close(top, just_below, 1e-5) && close(top, just_above, 1e-5));
let mut psi = Wavefunction1D::plane_wave(1.0, 0.1, 0.0, 32).unwrap();
assert!(tdse_split_operator(&mut psi, &[0.0; 4], 0.1, 1, 1.0, 1.0).is_err());
assert!(tdse_split_operator(&mut psi, &[0.0; 32], 0.1, 1, 0.0, 1.0).is_err());
assert!(tdse_crank_nicolson(&mut psi, &[0.0; 4], 0.1, 1, 1.0, 1.0).is_err());
assert!(tdse_crank_nicolson(&mut psi, &[0.0; 32], 0.1, 1, -1.0, 1.0).is_err());
let mut odd = Wavefunction1D::plane_wave(1.0, 0.1, 0.0, 30).unwrap();
assert!(tdse_split_operator(&mut odd, &[0.0; 30], 0.1, 1, 1.0, 1.0).is_err());
assert!(tdse_crank_nicolson(&mut odd, &[0.0; 30], 0.01, 1, 1.0, 1.0).is_ok());
assert!(wavepacket_scattering(&[0.0; 30], 0.1, 0.0, 0.0, 1.0, 1.0, -1.0, 0.01, 1, 1.0, 1.0)
.is_err());
}
#[test]
#[should_panic(expected = "positive amplitude and width")]
fn the_soliton_rejects_a_zero_width() {
let _ = soliton_bright_exact(0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0);
}
#[test]
#[should_panic(expected = "positive parameters")]
fn the_revival_time_rejects_a_zero_box() {
let _ = revival_time(0.0, 1.0, 1.0);
}
}