use crate::error::GeomError;
use crate::linalg::matrix::Matrix;
use crate::monte_carlo::Rng;
const HBAR: f64 = 1.054_571_817e-34;
const BOLTZMANN: f64 = 1.380_649e-23;
const ELEMENTARY_CHARGE: f64 = 1.602_176_634e-19;
const ELECTRON_MASS: f64 = 9.109_383_701_5e-31;
const EPSILON_0: f64 = 8.854_187_812_8e-12;
pub fn tight_binding_1d(
t_hop: f64,
on_site: &[f64],
periodic: bool,
) -> Result<(Vec<f64>, Vec<Vec<f64>>), GeomError> {
let n = on_site.len();
if !(2..=500).contains(&n) {
return Err(GeomError::InvalidArgument("the chain needs 2 to 500 sites"));
}
if periodic {
let mut m = Matrix::zeros(n, n);
for i in 0..n {
m.set(i, i, on_site[i]);
let next = (i + 1) % n;
m.set(i, next, m.get(i, next) - t_hop);
m.set(next, i, m.get(next, i) - t_hop);
}
let decomposition = crate::linalg::eigen::eigen_symmetric(&m, 1e-13, 300)
.map_err(|_| GeomError::Degenerate("the tight-binding eigenproblem failed"))?;
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
decomposition.values[a]
.partial_cmp(&decomposition.values[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
let values: Vec<f64> = order.iter().map(|&i| decomposition.values[i]).collect();
let vectors: Vec<Vec<f64>> = order
.iter()
.map(|&i| (0..n).map(|k| decomposition.vectors.get(k, i)).collect())
.collect();
return Ok((values, vectors));
}
let off = vec![-t_hop; n - 1];
crate::linalg::tridiagonal::eigen_symmetric_tridiagonal(on_site, &off)
.map_err(|_| GeomError::Degenerate("the tight-binding eigenproblem failed"))
}
#[must_use]
pub fn tight_binding_band_1d(k: f64, t_hop: f64, a: f64) -> f64 {
-2.0 * t_hop * (k * a).cos()
}
pub fn ssh_model(cells: usize, t1: f64, t2: f64) -> Result<(Vec<f64>, Vec<Vec<f64>>), GeomError> {
if !(2..=200).contains(&cells) {
return Err(GeomError::InvalidArgument("the SSH chain needs 2 to 200 cells"));
}
let n = 2 * cells;
let diag = vec![0.0; n];
let off: Vec<f64> = (0..n - 1)
.map(|i| if i % 2 == 0 { -t1 } else { -t2 })
.collect();
crate::linalg::tridiagonal::eigen_symmetric_tridiagonal(&diag, &off)
.map_err(|_| GeomError::Degenerate("the SSH eigenproblem failed"))
}
#[must_use]
pub fn ssh_winding_number(t1: f64, t2: f64) -> i32 {
i32::from(t2.abs() > t1.abs())
}
pub fn ssh_edge_states(cells: usize, t1: f64, t2: f64) -> Result<usize, GeomError> {
let (energies, _) = ssh_model(cells, t1, t2)?;
let gap = 2.0 * (t1.abs() - t2.abs()).abs();
let threshold = (gap / 4.0).max(1e-9);
Ok(energies.iter().filter(|e| e.abs() < threshold).count())
}
pub fn tight_binding_square(nx: usize, ny: usize, t_hop: f64) -> Result<Vec<f64>, GeomError> {
if nx == 0 || ny == 0 || nx * ny > 40_000 {
return Err(GeomError::InvalidArgument("the lattice size is out of range"));
}
let mut out = Vec::with_capacity(nx * ny);
for i in 1..=nx {
for j in 1..=ny {
let kx = i as f64 * std::f64::consts::PI / (nx + 1) as f64;
let ky = j as f64 * std::f64::consts::PI / (ny + 1) as f64;
out.push(-2.0 * t_hop * (kx.cos() + ky.cos()));
}
}
out.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
Ok(out)
}
#[must_use]
pub fn graphene_dispersion(kx: f64, ky: f64, t_hop: f64) -> (f64, f64) {
let sqrt3 = 3.0f64.sqrt();
let magnitude = (1.0
+ 4.0 * (sqrt3 * ky / 2.0).cos() * (3.0 * kx / 2.0).cos()
+ 4.0 * (sqrt3 * ky / 2.0).cos().powi(2))
.max(0.0)
.sqrt();
(-t_hop * magnitude, t_hop * magnitude)
}
#[must_use]
pub fn dirac_points_graphene() -> Vec<(f64, f64)> {
let sqrt3 = 3.0f64.sqrt();
let a = 2.0 * std::f64::consts::PI / 3.0;
let b = 2.0 * std::f64::consts::PI / (3.0 * sqrt3);
vec![
(a, b),
(a, -b),
(-a, b),
(-a, -b),
(0.0, 2.0 * b),
(0.0, -2.0 * b),
]
}
pub fn kronig_penney(
v0: f64,
a: f64,
b: f64,
energy: f64,
mass: f64,
hbar: f64,
) -> Result<f64, GeomError> {
if !(a > 0.0) || !(b > 0.0) || !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("kronig_penney: bad parameters"));
}
let factor = 2.0 * mass / (hbar * hbar);
let alpha = (factor * energy).abs().sqrt();
if energy < v0 {
let beta = (factor * (v0 - energy)).sqrt();
if alpha == 0.0 || beta == 0.0 {
return Ok(f64::INFINITY);
}
let term = (beta * beta - alpha * alpha) / (2.0 * alpha * beta);
Ok(term * (beta * b).sinh() * (alpha * a).sin() + (beta * b).cosh() * (alpha * a).cos())
} else {
let beta = (factor * (energy - v0)).sqrt();
if alpha == 0.0 || beta == 0.0 {
return Ok(f64::INFINITY);
}
let term = -(beta * beta + alpha * alpha) / (2.0 * alpha * beta);
Ok(term * (beta * b).sin() * (alpha * a).sin() + (beta * b).cos() * (alpha * a).cos())
}
}
pub fn kronig_penney_bands(
v0: f64,
a: f64,
b: f64,
energy_range: (f64, f64),
samples: usize,
mass: f64,
hbar: f64,
) -> Result<Vec<(f64, f64)>, GeomError> {
let (lo, hi) = energy_range;
if !(hi > lo) || samples < 2 {
return Err(GeomError::InvalidArgument("kronig_penney_bands: bad range"));
}
let mut bands: Vec<(f64, f64)> = Vec::new();
let mut inside: Option<f64> = None;
for k in 0..=samples {
let energy = lo + (hi - lo) * k as f64 / samples as f64;
let value = kronig_penney(v0, a, b, energy, mass, hbar)?;
let allowed = value.abs() <= 1.0;
match (allowed, inside) {
(true, None) => inside = Some(energy),
(false, Some(start)) => {
bands.push((start, energy));
inside = None;
}
_ => {}
}
}
if let Some(start) = inside {
bands.push((start, hi));
}
Ok(bands)
}
pub fn density_of_states_1d_free(energy: f64, mass: f64, hbar: f64) -> Result<f64, GeomError> {
if !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("the mass and hbar must be positive"));
}
if energy <= 0.0 {
return Ok(0.0);
}
Ok((2.0 * mass).sqrt() / (std::f64::consts::PI * hbar * energy.sqrt()))
}
pub fn density_of_states_2d_free(energy: f64, mass: f64, hbar: f64) -> Result<f64, GeomError> {
if !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("the mass and hbar must be positive"));
}
if energy <= 0.0 {
return Ok(0.0);
}
Ok(mass / (std::f64::consts::PI * hbar * hbar))
}
pub fn density_of_states_3d_free(energy: f64, mass: f64, hbar: f64) -> Result<f64, GeomError> {
if !(mass > 0.0) || !(hbar > 0.0) {
return Err(GeomError::InvalidArgument("the mass and hbar must be positive"));
}
if energy <= 0.0 {
return Ok(0.0);
}
let prefactor = (2.0 * mass).powf(1.5) / (2.0 * std::f64::consts::PI.powi(2) * hbar.powi(3));
Ok(prefactor * energy.sqrt())
}
pub fn dos_from_bands(
levels: &[f64],
sigma: f64,
points: usize,
) -> Result<Vec<(f64, f64)>, GeomError> {
if levels.is_empty() || !(sigma > 0.0) || points < 2 {
return Err(GeomError::InvalidArgument("dos_from_bands: bad input"));
}
let lo = levels.iter().copied().fold(f64::INFINITY, f64::min) - 4.0 * sigma;
let hi = levels.iter().copied().fold(f64::NEG_INFINITY, f64::max) + 4.0 * sigma;
let norm = 1.0 / (sigma * (2.0 * std::f64::consts::PI).sqrt());
Ok((0..points)
.map(|k| {
let energy = lo + (hi - lo) * k as f64 / (points - 1) as f64;
let density: f64 = levels
.iter()
.map(|e| norm * (-(energy - e).powi(2) / (2.0 * sigma * sigma)).exp())
.sum();
(energy, density)
})
.collect())
}
pub fn fermi_dirac(energy: f64, mu: f64, temperature: f64) -> Result<f64, GeomError> {
if temperature < 0.0 {
return Err(GeomError::InvalidArgument("the temperature cannot be negative"));
}
if temperature == 0.0 {
return Ok(if energy < mu {
1.0
} else if energy > mu {
0.0
} else {
0.5
});
}
let x = (energy - mu) / (BOLTZMANN * temperature);
Ok(if x > 0.0 {
let e = (-x).exp();
e / (1.0 + e)
} else {
1.0 / (1.0 + x.exp())
})
}
pub fn bose_einstein(energy: f64, mu: f64, temperature: f64) -> Result<f64, GeomError> {
if temperature < 0.0 {
return Err(GeomError::InvalidArgument("the temperature cannot be negative"));
}
if energy <= mu {
return Err(GeomError::InvalidArgument("bosons require the energy above mu"));
}
if temperature == 0.0 {
return Ok(0.0);
}
let x = (energy - mu) / (BOLTZMANN * temperature);
Ok(1.0 / (x.exp() - 1.0))
}
pub fn fermi_energy_free(density: f64, mass: f64) -> Result<f64, GeomError> {
if !(density > 0.0) || !(mass > 0.0) {
return Err(GeomError::InvalidArgument("the density and mass must be positive"));
}
let k_f = (3.0 * std::f64::consts::PI * std::f64::consts::PI * density).powf(1.0 / 3.0);
Ok(HBAR * HBAR * k_f * k_f / (2.0 * mass))
}
pub fn sommerfeld_heat_capacity(temperature: f64, fermi_temperature: f64) -> Result<f64, GeomError> {
if !(fermi_temperature > 0.0) || temperature < 0.0 {
return Err(GeomError::InvalidArgument("sommerfeld_heat_capacity: bad temperatures"));
}
Ok(std::f64::consts::PI * std::f64::consts::PI / 2.0 * BOLTZMANN * temperature
/ fermi_temperature)
}
pub fn debye_heat_capacity(temperature: f64, debye_temperature: f64) -> Result<f64, GeomError> {
if !(debye_temperature > 0.0) || temperature < 0.0 {
return Err(GeomError::InvalidArgument("debye_heat_capacity: bad temperatures"));
}
if temperature == 0.0 {
return Ok(0.0);
}
let ratio = temperature / debye_temperature;
let upper = 1.0 / ratio;
let samples = 4000usize;
let h = upper / samples as f64;
let integral: f64 = (0..samples)
.map(|k| {
let x = (k as f64 + 0.5) * h;
let e = x.exp();
if !e.is_finite() {
return 0.0;
}
x.powi(4) * e / (e - 1.0).powi(2)
})
.sum::<f64>()
* h;
Ok(9.0 * BOLTZMANN * ratio.powi(3) * integral)
}
pub fn einstein_heat_capacity(
temperature: f64,
einstein_temperature: f64,
) -> Result<f64, GeomError> {
if !(einstein_temperature > 0.0) || temperature < 0.0 {
return Err(GeomError::InvalidArgument("einstein_heat_capacity: bad temperatures"));
}
if temperature == 0.0 {
return Ok(0.0);
}
let x = einstein_temperature / temperature;
if x > 700.0 {
return Ok(0.0);
}
let e = x.exp();
Ok(3.0 * BOLTZMANN * x * x * e / (e - 1.0).powi(2))
}
#[must_use]
pub fn phonon_dispersion_1d_monatomic(k: f64, spring: f64, mass: f64, a: f64) -> f64 {
assert!(spring > 0.0 && mass > 0.0, "the spring constant and mass must be positive");
2.0 * (spring / mass).sqrt() * (k * a / 2.0).sin().abs()
}
#[must_use]
pub fn phonon_dispersion_1d_diatomic(
k: f64,
spring: f64,
m1: f64,
m2: f64,
a: f64,
) -> (f64, f64) {
assert!(spring > 0.0 && m1 > 0.0 && m2 > 0.0, "the parameters must be positive");
let sum = 1.0 / m1 + 1.0 / m2;
let inner = sum * sum - 4.0 * (k * a).sin().powi(2) / (m1 * m2);
let root = inner.max(0.0).sqrt();
let acoustic = (spring * (sum - root)).max(0.0).sqrt();
let optical = (spring * (sum + root)).max(0.0).sqrt();
(acoustic, optical)
}
pub fn bloch_oscillation_period(field: f64, a: f64) -> Result<f64, GeomError> {
if !(field > 0.0) || !(a > 0.0) {
return Err(GeomError::InvalidArgument("the field and spacing must be positive"));
}
Ok(2.0 * std::f64::consts::PI * HBAR / (ELEMENTARY_CHARGE * field * a))
}
pub fn landau_levels(field: f64, n: usize, mass: f64) -> Result<f64, GeomError> {
if !(field > 0.0) || !(mass > 0.0) {
return Err(GeomError::InvalidArgument("the field and mass must be positive"));
}
let cyclotron = ELEMENTARY_CHARGE * field / mass;
Ok((n as f64 + 0.5) * HBAR * cyclotron)
}
pub fn hofstadter_butterfly(q_max: usize, k_samples: usize) -> Result<Vec<(f64, f64)>, GeomError> {
if !(2..=40).contains(&q_max) || k_samples == 0 {
return Err(GeomError::InvalidArgument("hofstadter_butterfly: bad parameters"));
}
let mut out = Vec::new();
for q in 2..=q_max {
for p in 1..q {
if gcd(p, q) != 1 {
continue;
}
let flux = p as f64 / q as f64;
for s in 0..k_samples {
let ky = 2.0 * std::f64::consts::PI * s as f64 / (k_samples * q) as f64;
let mut m = Matrix::zeros(q, q);
for j in 0..q {
m.set(
j,
j,
2.0 * (2.0 * std::f64::consts::PI * flux * j as f64 + ky).cos(),
);
let next = (j + 1) % q;
if q > 2 {
m.set(j, next, m.get(j, next) + 1.0);
m.set(next, j, m.get(next, j) + 1.0);
} else if j == 0 {
m.set(0, 1, 2.0);
m.set(1, 0, 2.0);
}
}
let decomposition = crate::linalg::eigen::eigen_symmetric(&m, 1e-12, 200)
.map_err(|_| GeomError::Degenerate("the Harper eigenproblem failed"))?;
for e in &decomposition.values {
out.push((flux, *e));
}
}
}
}
Ok(out)
}
fn gcd(mut a: usize, mut b: usize) -> usize {
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a
}
#[must_use]
pub fn quantum_hall_conductance(filled: usize) -> f64 {
filled as f64 * ELEMENTARY_CHARGE * ELEMENTARY_CHARGE
/ (2.0 * std::f64::consts::PI * HBAR)
}
pub fn drude_conductivity(density: f64, tau: f64, mass: f64) -> Result<f64, GeomError> {
if !(tau > 0.0) || !(mass > 0.0) || density < 0.0 {
return Err(GeomError::InvalidArgument("drude_conductivity: bad parameters"));
}
Ok(density * ELEMENTARY_CHARGE * ELEMENTARY_CHARGE * tau / mass)
}
pub fn hall_coefficient(density: f64, charge: f64) -> Result<f64, GeomError> {
if density == 0.0 || charge == 0.0 {
return Err(GeomError::InvalidArgument("hall_coefficient needs carriers"));
}
Ok(1.0 / (density * charge))
}
pub fn effective_mass_from_band(
band: &dyn Fn(f64) -> f64,
k0: f64,
h: f64,
) -> Result<f64, GeomError> {
if !(h > 0.0) {
return Err(GeomError::InvalidArgument("the step must be positive"));
}
let curvature = (band(k0 + h) - 2.0 * band(k0) + band(k0 - h)) / (h * h);
let scale = (band(k0 + h).abs() + band(k0).abs() + band(k0 - h).abs()) / (h * h);
if curvature.abs() <= 1e-12 * scale {
return Err(GeomError::Degenerate("the band is flat here"));
}
Ok(HBAR * HBAR / curvature)
}
pub fn semiconductor_carrier_density(
gap_ev: f64,
temperature: f64,
m_electron: f64,
m_hole: f64,
) -> Result<f64, GeomError> {
if !(temperature > 0.0) || !(m_electron > 0.0) || !(m_hole > 0.0) {
return Err(GeomError::InvalidArgument("semiconductor_carrier_density: bad parameters"));
}
let kt = BOLTZMANN * temperature;
let prefactor = |m: f64| 2.0 * (m * kt / (2.0 * std::f64::consts::PI * HBAR * HBAR)).powf(1.5);
let nc = prefactor(m_electron * ELECTRON_MASS);
let nv = prefactor(m_hole * ELECTRON_MASS);
Ok((nc * nv).sqrt() * (-gap_ev * ELEMENTARY_CHARGE / (2.0 * kt)).exp())
}
pub fn pn_junction_builtin(
acceptors: f64,
donors: f64,
intrinsic: f64,
temperature: f64,
) -> Result<f64, GeomError> {
if !(acceptors > 0.0) || !(donors > 0.0) || !(intrinsic > 0.0) || !(temperature > 0.0) {
return Err(GeomError::InvalidArgument("pn_junction_builtin: bad parameters"));
}
let thermal = BOLTZMANN * temperature / ELEMENTARY_CHARGE;
Ok(thermal * (acceptors * donors / (intrinsic * intrinsic)).ln())
}
pub fn depletion_width(
built_in: f64,
acceptors: f64,
donors: f64,
relative_permittivity: f64,
) -> Result<f64, GeomError> {
if !(acceptors > 0.0) || !(donors > 0.0) || !(relative_permittivity > 0.0) || built_in < 0.0 {
return Err(GeomError::InvalidArgument("depletion_width: bad parameters"));
}
let epsilon = relative_permittivity * EPSILON_0;
Ok((2.0 * epsilon * built_in / ELEMENTARY_CHARGE
* (1.0 / acceptors + 1.0 / donors))
.sqrt())
}
pub fn bcs_gap_equation(temperature: f64, critical_temperature: f64) -> Result<f64, GeomError> {
if !(critical_temperature > 0.0) || temperature < 0.0 {
return Err(GeomError::InvalidArgument("bcs_gap_equation: bad temperatures"));
}
if temperature >= critical_temperature {
return Ok(0.0);
}
let t = temperature / critical_temperature;
if t <= 0.0 {
return Ok(1.0);
}
Ok((1.74 * (1.0 / t - 1.0).max(0.0).sqrt()).tanh())
}
pub fn bcs_tc_from_coupling(coupling: f64, debye_temperature: f64) -> Result<f64, GeomError> {
if !(coupling > 0.0) || !(debye_temperature > 0.0) {
return Err(GeomError::InvalidArgument("bcs_tc_from_coupling: bad parameters"));
}
Ok(1.14 * debye_temperature * (-1.0 / coupling).exp())
}
#[must_use]
pub fn josephson_current(critical_current: f64, phase: f64) -> f64 {
critical_current * phase.sin()
}
#[must_use]
pub fn josephson_frequency(voltage: f64) -> f64 {
2.0 * ELEMENTARY_CHARGE * voltage / (2.0 * std::f64::consts::PI * HBAR)
}
pub fn anderson_localization_1d(
n: usize,
disorder: f64,
energy: f64,
trials: usize,
rng: &mut Rng,
) -> Result<f64, GeomError> {
if n < 10 || !(disorder > 0.0) || trials == 0 {
return Err(GeomError::InvalidArgument("anderson_localization_1d: bad parameters"));
}
let mut total = 0.0;
for _ in 0..trials {
let (mut a, mut b) = (1.0f64, 0.0f64);
let mut log_growth = 0.0;
for _ in 0..n {
let on_site = disorder * (rng.next_f64() - 0.5);
let next = (energy - on_site) * a - b;
b = a;
a = next;
let magnitude = a.hypot(b);
if magnitude > 0.0 {
log_growth += magnitude.ln();
a /= magnitude;
b /= magnitude;
}
}
total += log_growth / n as f64;
}
let lyapunov = total / trials as f64;
if lyapunov <= 0.0 {
return Err(GeomError::Degenerate("the Lyapunov exponent did not come out positive"));
}
Ok(1.0 / lyapunov)
}
pub fn conductance_landauer(transmissions: &[f64]) -> Result<f64, GeomError> {
if transmissions.iter().any(|t| !(0.0..=1.0).contains(t)) {
return Err(GeomError::InvalidArgument("a transmission is not a probability"));
}
let quantum = 2.0 * ELEMENTARY_CHARGE * ELEMENTARY_CHARGE
/ (2.0 * std::f64::consts::PI * HBAR);
Ok(quantum * transmissions.iter().sum::<f64>())
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn relative(a: f64, b: f64) -> f64 {
(a - b).abs() / b.abs().max(1e-300)
}
#[test]
fn the_tight_binding_chain_matches_its_closed_form_spectrum() {
let t = 1.3f64;
for n in [2usize, 5, 12, 40] {
let (energies, vectors) = tight_binding_1d(t, &vec![0.0; n], false).unwrap();
let mut expected: Vec<f64> = (1..=n)
.map(|m| -2.0 * t * (m as f64 * std::f64::consts::PI / (n + 1) as f64).cos())
.collect();
expected.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
for (got, want) in energies.iter().zip(&expected) {
assert!(close(*got, *want, 1e-9), "open n = {n}: {energies:?} against {expected:?}");
}
for i in 0..n {
for j in 0..n {
let overlap: f64 =
vectors[i].iter().zip(&vectors[j]).map(|(a, b)| a * b).sum();
assert!(close(overlap, f64::from(i == j), 1e-9));
}
}
if n > 2 {
let (ring, _) = tight_binding_1d(t, &vec![0.0; n], true).unwrap();
let mut expected: Vec<f64> = (0..n)
.map(|m| -2.0 * t * (2.0 * std::f64::consts::PI * m as f64 / n as f64).cos())
.collect();
expected.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
for (got, want) in ring.iter().zip(&expected) {
assert!(close(*got, *want, 1e-8), "ring n = {n}: {ring:?} against {expected:?}");
}
}
}
let n = 60usize;
let (energies, _) = tight_binding_1d(t, &vec![0.0; n], false).unwrap();
for m in 1..=n {
let k = m as f64 * std::f64::consts::PI / ((n + 1) as f64);
let band = tight_binding_band_1d(k, t, 1.0);
assert!(
energies.iter().any(|e| close(*e, band, 1e-9)),
"the band value {band} at k = {k} is not a level"
);
}
let bottom = tight_binding_band_1d(0.0, t, 2.7);
let top = tight_binding_band_1d(std::f64::consts::PI / 2.7, t, 2.7);
assert!(close(top - bottom, 4.0 * t, 1e-9), "the bandwidth is {}", top - bottom);
assert!(tight_binding_1d(t, &[1.0], false).is_err());
assert!(tight_binding_1d(t, &vec![0.0; 600], false).is_err());
}
#[test]
fn the_ssh_chain_has_edge_states_exactly_when_its_winding_number_says_so() {
for (t1, t2) in [(1.0f64, 2.0f64), (2.0, 1.0), (0.5, 3.0), (3.0, 0.5), (1.0, 1.0)] {
let winding = ssh_winding_number(t1, t2);
let states = ssh_edge_states(40, t1, t2).unwrap();
if (t1 - t2).abs() < 1e-12 {
assert_eq!(winding, 0);
continue;
}
assert_eq!(
states,
2 * winding as usize,
"t1 = {t1}, t2 = {t2}: winding {winding} but {states} edge states"
);
}
let (energies, vectors) = ssh_model(40, 0.5, 2.0).unwrap();
let zero_modes: Vec<usize> = (0..energies.len())
.filter(|&i| energies[i].abs() < 0.1)
.collect();
assert_eq!(zero_modes.len(), 2, "expected two zero modes, got {}", zero_modes.len());
for &i in &zero_modes {
let weight: f64 = vectors[i].iter().map(|c| c * c).sum();
let edges: f64 = vectors[i][..6].iter().map(|c| c * c).sum::<f64>()
+ vectors[i][74..].iter().map(|c| c * c).sum::<f64>();
assert!(
edges / weight > 0.9,
"the zero mode has only {} of its weight at the edges",
edges / weight
);
}
let gap = energies
.iter()
.filter(|e| **e > 0.2)
.fold(f64::INFINITY, |acc, e| acc.min(*e))
* 2.0;
assert!(close(gap, 2.0 * 1.5, 0.05), "the gap is {gap}");
assert!(ssh_model(1, 1.0, 2.0).is_err());
assert!(ssh_edge_states(1, 1.0, 2.0).is_err());
}
#[test]
fn the_square_lattice_is_separable_and_graphene_closes_its_gap_at_the_dirac_points() {
let t = 1.0f64;
let levels = tight_binding_square(6, 6, t).unwrap();
assert_eq!(levels.len(), 36);
assert!(levels.windows(2).all(|w| w[0] <= w[1] + 1e-12));
assert!(levels[0] > -4.0 * t && levels[35] < 4.0 * t);
for (low, high) in levels.iter().zip(levels.iter().rev()) {
assert!(close(*low, -high, 1e-9), "the spectrum is not symmetric");
}
for &(kx, ky) in &dirac_points_graphene() {
let (lower, upper) = graphene_dispersion(kx, ky, t);
assert!(
close(upper - lower, 0.0, 1e-9),
"the gap at ({kx}, {ky}) is {}",
upper - lower
);
}
let (kx, ky) = dirac_points_graphene()[0];
let mut previous = 0.0;
for delta in [0.005f64, 0.01, 0.02, 0.04] {
let (lower, upper) = graphene_dispersion(kx + delta, ky, t);
let gap = upper - lower;
assert!(gap > previous, "the gap did not grow at delta = {delta}");
if previous > 0.0 {
let ratio = gap / previous;
assert!(
(1.9..2.1).contains(&ratio),
"doubling the distance changed the gap by {ratio}, not linearly"
);
}
previous = gap;
}
let (lower, upper) = graphene_dispersion(0.0, 0.0, t);
assert!(close(upper, 3.0 * t, 1e-9) && close(lower, -3.0 * t, 1e-9));
assert!(tight_binding_square(0, 5, t).is_err());
assert!(tight_binding_square(500, 500, t).is_err());
}
#[test]
fn the_kronig_penney_lattice_has_bands_that_widen_as_the_barrier_falls() {
let (a, b, mass, hbar) = (1.0f64, 0.3f64, 1.0f64, 1.0f64);
let free = kronig_penney_bands(0.0, a, b, (0.01, 60.0), 40_000, mass, hbar).unwrap();
let free_width: f64 = free.iter().map(|(lo, hi)| hi - lo).sum();
assert!(
free_width > 59.0,
"with no barrier almost everything should be allowed, got {free_width}"
);
let mut previous = free_width;
for v0 in [2.0f64, 10.0, 40.0, 150.0] {
let bands = kronig_penney_bands(v0, a, b, (0.01, 60.0), 40_000, mass, hbar).unwrap();
let width: f64 = bands.iter().map(|(lo, hi)| hi - lo).sum();
assert!(
width < previous,
"raising the barrier to {v0} widened the allowed set to {width}"
);
assert!(!bands.is_empty(), "every barrier leaves some bands");
for (lo, hi) in &bands {
let middle = 0.5 * (lo + hi);
let value = kronig_penney(v0, a, b, middle, mass, hbar).unwrap();
assert!(
value.abs() <= 1.0 + 1e-9,
"the middle of a band has |f| = {}",
value.abs()
);
}
previous = width;
}
assert!(previous < free_width / 2.0, "a tall barrier should narrow the bands sharply");
assert!(kronig_penney(1.0, 0.0, 1.0, 1.0, 1.0, 1.0).is_err());
assert!(kronig_penney_bands(1.0, 1.0, 1.0, (2.0, 1.0), 100, 1.0, 1.0).is_err());
}
#[test]
fn the_free_electron_densities_of_states_have_the_dimensional_dependence_they_should() {
let (m, hbar) = (1.0f64, 1.0f64);
let one_a = density_of_states_1d_free(1.0, m, hbar).unwrap();
let one_b = density_of_states_1d_free(4.0, m, hbar).unwrap();
assert!(close(one_a / one_b, 2.0, 1e-9), "the one-dimensional ratio is {}", one_a / one_b);
let two_a = density_of_states_2d_free(1.0, m, hbar).unwrap();
let two_b = density_of_states_2d_free(9.0, m, hbar).unwrap();
assert!(close(two_a, two_b, 1e-12), "the two-dimensional density is not constant");
let three_a = density_of_states_3d_free(1.0, m, hbar).unwrap();
let three_b = density_of_states_3d_free(4.0, m, hbar).unwrap();
assert!(close(three_b / three_a, 2.0, 1e-9), "the three-dimensional ratio is wrong");
for f in [
density_of_states_1d_free as fn(f64, f64, f64) -> Result<f64, GeomError>,
density_of_states_2d_free,
density_of_states_3d_free,
] {
assert_eq!(f(-1.0, m, hbar).unwrap(), 0.0);
assert_eq!(f(0.0, m, hbar).unwrap(), 0.0);
assert!(f(1.0, 0.0, hbar).is_err());
}
let density = 8.5e28f64;
let fermi = fermi_energy_free(density, ELECTRON_MASS).unwrap();
let samples = 200_000usize;
let h = fermi / samples as f64;
let integral: f64 = (0..samples)
.map(|k| {
density_of_states_3d_free((k as f64 + 0.5) * h, ELECTRON_MASS, HBAR).unwrap()
})
.sum::<f64>()
* h;
assert!(
relative(integral, density) < 1e-4,
"the density of states integrates to {integral}, not {density}"
);
assert!(
(fermi / ELEMENTARY_CHARGE - 7.0).abs() < 0.5,
"the Fermi energy is {} electronvolts",
fermi / ELEMENTARY_CHARGE
);
assert!(fermi_energy_free(0.0, ELECTRON_MASS).is_err());
}
#[test]
fn the_occupations_have_the_limits_and_symmetries_they_should() {
let mu = 1.0e-19f64;
assert_eq!(fermi_dirac(mu * 0.5, mu, 0.0).unwrap(), 1.0);
assert_eq!(fermi_dirac(mu * 1.5, mu, 0.0).unwrap(), 0.0);
assert_eq!(fermi_dirac(mu, mu, 0.0).unwrap(), 0.5);
for t in [1.0f64, 300.0, 5000.0] {
assert!(close(fermi_dirac(mu, mu, t).unwrap(), 0.5, 1e-15));
for delta in [1e-21f64, 1e-20, 5e-20] {
let above = fermi_dirac(mu + delta, mu, t).unwrap();
let below = fermi_dirac(mu - delta, mu, t).unwrap();
assert!(close(above + below, 1.0, 1e-12), "the function is not antisymmetric");
assert!((0.0..=1.0).contains(&above));
}
let far = fermi_dirac(mu + 100.0 * BOLTZMANN * t, mu, t).unwrap();
assert!(far > 0.0 && far < 1e-40, "the tail is {far}");
}
let mut previous = 0.0;
for delta in [1e-20f64, 1e-21, 1e-22] {
let n = bose_einstein(mu + delta, mu, 300.0).unwrap();
assert!(n > previous, "the occupation fell as the gap closed");
previous = n;
}
let energy = mu + 1e-21;
let classical = BOLTZMANN * 1e6 / (energy - mu);
assert!(
relative(bose_einstein(energy, mu, 1e6).unwrap(), classical) < 1e-3,
"the classical limit fails"
);
assert!(bose_einstein(mu, mu, 300.0).is_err());
assert!(bose_einstein(mu * 2.0, mu, -1.0).is_err());
assert!(fermi_dirac(mu, mu, -1.0).is_err());
}
#[test]
fn a_broadened_spectrum_integrates_to_the_number_of_levels_it_came_from() {
let levels = [-2.0f64, -1.0, -1.0, 0.5, 3.0];
let curve = dos_from_bands(&levels, 0.1, 4000).unwrap();
let h = curve[1].0 - curve[0].0;
let total: f64 = curve.iter().map(|(_, d)| d).sum::<f64>() * h;
assert!(
relative(total, levels.len() as f64) < 1e-3,
"the density integrates to {total}, not {}",
levels.len()
);
let at = |e: f64| {
curve
.iter()
.min_by(|a, b| {
(a.0 - e).abs().partial_cmp(&(b.0 - e).abs()).unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap()
.1
};
assert!(
(at(-1.0) / at(3.0) - 2.0).abs() < 0.05,
"the ratio is {}",
at(-1.0) / at(3.0)
);
assert!(dos_from_bands(&[], 0.1, 100).is_err());
assert!(dos_from_bands(&levels, 0.0, 100).is_err());
}
#[test]
fn debye_goes_as_t_cubed_at_low_temperature_and_to_dulong_petit_at_high() {
let theta = 400.0f64;
for t in [4000.0f64, 20_000.0] {
let c = debye_heat_capacity(t, theta).unwrap();
assert!(
relative(c, 3.0 * BOLTZMANN) < 0.01,
"at {t} kelvin the capacity is {} against {}",
c,
3.0 * BOLTZMANN
);
}
for pair in [(4.0f64, 8.0f64), (8.0, 16.0), (2.0, 4.0)] {
let ratio =
debye_heat_capacity(pair.1, theta).unwrap() / debye_heat_capacity(pair.0, theta).unwrap();
assert!(
(ratio - 8.0).abs() < 0.05,
"doubling from {} to {} changed the capacity by {ratio}, not eight",
pair.0,
pair.1
);
}
let t = 4.0f64;
let predicted = 12.0 * std::f64::consts::PI.powi(4) / 5.0 * BOLTZMANN * (t / theta).powi(3);
assert!(
relative(debye_heat_capacity(t, theta).unwrap(), predicted) < 0.01,
"the low-temperature coefficient is off: {} against {predicted}",
debye_heat_capacity(t, theta).unwrap()
);
assert_eq!(debye_heat_capacity(0.0, theta).unwrap(), 0.0);
assert!(debye_heat_capacity(1.0, 0.0).is_err());
assert!(relative(einstein_heat_capacity(50_000.0, theta).unwrap(), 3.0 * BOLTZMANN) < 0.01);
let low_einstein = einstein_heat_capacity(20.0, theta).unwrap();
let low_debye = debye_heat_capacity(20.0, theta).unwrap();
assert!(
low_einstein < low_debye / 10.0,
"Einstein should fall far faster: {low_einstein} against {low_debye}"
);
assert_eq!(einstein_heat_capacity(0.0, theta).unwrap(), 0.0);
assert!(einstein_heat_capacity(1.0, -1.0).is_err());
let fermi_temperature = 8.0e4f64;
let electronic = sommerfeld_heat_capacity(300.0, fermi_temperature).unwrap();
assert!(
close(
sommerfeld_heat_capacity(600.0, fermi_temperature).unwrap(),
2.0 * electronic,
1e-28
),
"the electronic capacity is not linear"
);
assert!(
electronic < 0.05 * 3.0 * BOLTZMANN,
"the electronic capacity is {electronic}, not small against the lattice"
);
assert!(sommerfeld_heat_capacity(300.0, 0.0).is_err());
}
#[test]
fn phonons_are_linear_at_long_wavelength_and_the_diatomic_chain_opens_a_gap() {
let (spring, mass, a) = (4.0f64, 2.0f64, 1.0f64);
let sound = (spring / mass).sqrt() * a;
for k in [0.001f64, 0.002, 0.004] {
let omega = phonon_dispersion_1d_monatomic(k, spring, mass, a);
assert!(
relative(omega, sound * k) < 1e-4,
"at k = {k} the frequency is {omega}, the sound line {}",
sound * k
);
}
let top = phonon_dispersion_1d_monatomic(std::f64::consts::PI / a, spring, mass, a);
assert!(close(top, 2.0 * (spring / mass).sqrt(), 1e-12));
assert!(close(phonon_dispersion_1d_monatomic(0.0, spring, mass, a), 0.0, 1e-15));
let (m1, m2) = (1.0f64, 3.0f64);
let (acoustic0, optical0) = phonon_dispersion_1d_diatomic(0.0, spring, m1, m2, a);
assert!(close(acoustic0, 0.0, 1e-12), "the acoustic branch starts at {acoustic0}");
assert!(optical0 > 0.0, "the optical branch starts at zero");
assert!(
close(optical0, (2.0 * spring * (1.0 / m1 + 1.0 / m2)).sqrt(), 1e-9),
"the optical branch at k = 0 is {optical0}"
);
for steps in 0..40usize {
let k = std::f64::consts::PI / (2.0 * a) * steps as f64 / 40.0;
let (acoustic, optical) = phonon_dispersion_1d_diatomic(k, spring, m1, m2, a);
assert!(acoustic <= optical + 1e-12, "the branches crossed at k = {k}");
assert!(acoustic >= 0.0 && optical >= 0.0);
}
let (a_eq, o_eq) = phonon_dispersion_1d_diatomic(
std::f64::consts::PI / (2.0 * a),
spring,
mass,
mass,
a,
);
assert!(
close(a_eq, o_eq, 1e-9),
"equal masses should close the gap: {a_eq} against {o_eq}"
);
}
#[test]
fn landau_levels_are_equally_spaced_and_the_hall_conductance_is_quantised() {
let field = 5.0f64;
let spacing = HBAR * ELEMENTARY_CHARGE * field / ELECTRON_MASS;
for n in 0..8usize {
let level = landau_levels(field, n, ELECTRON_MASS).unwrap();
assert!(
relative(level, (n as f64 + 0.5) * spacing) < 1e-12,
"level {n} is {level}"
);
if n > 0 {
let gap = level - landau_levels(field, n - 1, ELECTRON_MASS).unwrap();
assert!(relative(gap, spacing) < 1e-12, "the spacing is {gap}");
}
}
assert!(relative(
landau_levels(10.0, 0, ELECTRON_MASS).unwrap(),
2.0 * landau_levels(5.0, 0, ELECTRON_MASS).unwrap()
) < 1e-12);
assert!(landau_levels(0.0, 1, ELECTRON_MASS).is_err());
let quantum = quantum_hall_conductance(1);
assert!(
relative(quantum, 3.874_045_86e-5) < 1e-6,
"the conductance quantum is {quantum} siemens"
);
for n in 1..6usize {
assert!(relative(quantum_hall_conductance(n), n as f64 * quantum) < 1e-12);
}
assert_eq!(quantum_hall_conductance(0), 0.0);
assert!(relative(1.0 / quantum, 25_812.807) < 1e-6);
}
#[test]
fn the_hofstadter_spectrum_splits_into_as_many_bands_as_the_flux_denominator() {
let points = hofstadter_butterfly(6, 4).unwrap();
assert!(!points.is_empty());
for (flux, energy) in &points {
assert!((0.0..1.0).contains(flux), "the flux is {flux}");
assert!(energy.abs() <= 4.5, "an energy of {energy} is outside the band");
}
let half: Vec<f64> = points
.iter()
.filter(|(flux, _)| close(*flux, 0.5, 1e-12))
.map(|(_, e)| *e)
.collect();
assert!(!half.is_empty(), "half flux produced nothing");
let positive = half.iter().filter(|e| **e > 1e-9).count();
let negative = half.iter().filter(|e| **e < -1e-9).count();
assert_eq!(positive, negative, "the half-flux spectrum is not symmetric");
assert_eq!(positive * 2, half.len(), "half flux should have two sub-bands");
for (p, q) in [(1usize, 3usize), (1, 4), (2, 5)] {
let at: Vec<f64> = points
.iter()
.filter(|(flux, _)| close(*flux, p as f64 / q as f64, 1e-12))
.map(|(_, e)| *e)
.collect();
assert_eq!(
at.len() % q,
0,
"flux {p}/{q} gave {} energies, not a multiple of {q}",
at.len()
);
}
assert!(hofstadter_butterfly(1, 4).is_err());
assert!(hofstadter_butterfly(6, 0).is_err());
}
#[test]
fn the_effective_mass_is_positive_at_a_band_bottom_and_negative_at_the_top() {
let t = 1.0e-19f64;
let a = 3.0e-10f64;
let band = |k: f64| tight_binding_band_1d(k, t, a);
let bottom = effective_mass_from_band(&band, 0.0, 1e-4 / a).unwrap();
let expected = HBAR * HBAR / (2.0 * t * a * a);
assert!(
relative(bottom, expected) < 1e-4,
"the band-bottom mass is {bottom}, the closed form {expected}"
);
assert!(bottom > 0.0);
let top = effective_mass_from_band(&band, std::f64::consts::PI / a, 1e-4 / a).unwrap();
assert!(top < 0.0, "the band-top mass is {top}, not negative");
assert!(relative(top.abs(), expected) < 1e-4);
let free = |k: f64| HBAR * HBAR * k * k / (2.0 * ELECTRON_MASS);
let mass = effective_mass_from_band(&free, 1e9, 1e6).unwrap();
assert!(relative(mass, ELECTRON_MASS) < 1e-6, "the free mass came out {mass}");
assert!(effective_mass_from_band(&|_| 1.0, 0.0, 1e-3).is_err());
assert!(effective_mass_from_band(&band, 0.0, 0.0).is_err());
}
#[test]
fn a_bloch_oscillation_is_faster_in_a_stronger_field_and_a_wider_lattice() {
let period = bloch_oscillation_period(1e5, 1e-8).unwrap();
assert!(close(bloch_oscillation_period(2e5, 1e-8).unwrap(), period / 2.0, 1e-20));
assert!(close(bloch_oscillation_period(1e5, 2e-8).unwrap(), period / 2.0, 1e-20));
let ordinary = bloch_oscillation_period(1e5, 3e-10).unwrap();
assert!(
ordinary > 1e-10,
"the period is {ordinary} seconds, shorter than a scattering time"
);
assert!(bloch_oscillation_period(0.0, 1e-9).is_err());
assert!(bloch_oscillation_period(1e5, 0.0).is_err());
}
#[test]
fn drude_and_landauer_give_the_conductances_they_promise() {
let density = 8.5e28f64;
let sigma = drude_conductivity(density, 2.5e-14, ELECTRON_MASS).unwrap();
assert!(
(sigma / 6.0e7 - 1.0).abs() < 0.2,
"the conductivity is {sigma} siemens per metre"
);
assert!(close(
drude_conductivity(2.0 * density, 2.5e-14, ELECTRON_MASS).unwrap(),
2.0 * sigma,
1e-6 * sigma
));
assert!(drude_conductivity(density, 0.0, ELECTRON_MASS).is_err());
assert!(hall_coefficient(density, -ELEMENTARY_CHARGE).unwrap() < 0.0);
assert!(hall_coefficient(density, ELEMENTARY_CHARGE).unwrap() > 0.0);
assert!(hall_coefficient(0.0, ELEMENTARY_CHARGE).is_err());
let quantum = conductance_landauer(&[1.0]).unwrap();
assert!(
relative(quantum, 7.748_091_729e-5) < 1e-6,
"the conductance quantum is {quantum}"
);
assert!(relative(conductance_landauer(&[1.0; 4]).unwrap(), 4.0 * quantum) < 1e-12);
assert!(relative(conductance_landauer(&[0.5, 0.5]).unwrap(), quantum) < 1e-12);
assert_eq!(conductance_landauer(&[]).unwrap(), 0.0);
assert!(conductance_landauer(&[1.2]).is_err());
assert!(conductance_landauer(&[-0.1]).is_err());
}
#[test]
fn a_semiconductor_and_its_junction_behave_as_the_exponentials_say() {
let n300 = semiconductor_carrier_density(1.12, 300.0, 1.08, 0.81).unwrap();
assert!(
(n300.log10() - 16.0).abs() < 0.6,
"the intrinsic density is 10^{} per cubic metre",
n300.log10()
);
let n308 = semiconductor_carrier_density(1.12, 308.0, 1.08, 0.81).unwrap();
let ratio = n308 / n300;
assert!((1.8..2.6).contains(&ratio), "eight kelvin changed it by {ratio}");
let wide = semiconductor_carrier_density(3.3, 300.0, 1.0, 1.0).unwrap();
assert!(wide < n300 * 1e-15, "gallium nitride should be far more insulating");
assert!(semiconductor_carrier_density(1.1, 0.0, 1.0, 1.0).is_err());
let v0 = pn_junction_builtin(1e22, 1e22, n300, 300.0).unwrap();
assert!((0.3..1.12).contains(&v0), "the built-in potential is {v0} volts");
let heavier = pn_junction_builtin(1e24, 1e24, n300, 300.0).unwrap();
assert!(heavier > v0, "heavier doping should raise the barrier");
let thermal = BOLTZMANN * 300.0 / ELEMENTARY_CHARGE;
assert!(
close(heavier - v0, thermal * (1e4f64).ln(), 1e-9),
"the increase is {} volts",
heavier - v0
);
let wide_w = depletion_width(v0, 1e21, 1e21, 11.7).unwrap();
let narrow_w = depletion_width(v0, 1e24, 1e24, 11.7).unwrap();
assert!(narrow_w < wide_w, "heavier doping should narrow the depletion region");
assert!(
(wide_w / narrow_w - (1e3f64).sqrt()).abs() < 1e-6 * (1e3f64).sqrt(),
"the width should go as the inverse square root of the doping"
);
assert!((1e-8..1e-5).contains(&wide_w), "the width is {wide_w} metres");
assert!(pn_junction_builtin(0.0, 1e22, 1e16, 300.0).is_err());
assert!(depletion_width(1.0, 0.0, 1e22, 11.7).is_err());
}
#[test]
fn the_superconducting_gap_opens_as_a_square_root_and_the_josephson_relations_hold() {
let tc = 9.3f64;
assert!(close(bcs_gap_equation(0.0, tc).unwrap(), 1.0, 1e-12));
assert_eq!(bcs_gap_equation(tc, tc).unwrap(), 0.0);
assert_eq!(bcs_gap_equation(2.0 * tc, tc).unwrap(), 0.0);
let mut previous = 1.1;
for t in [0.1f64, 0.3, 0.5, 0.7, 0.9, 0.99] {
let gap = bcs_gap_equation(t * tc, tc).unwrap();
assert!(gap < previous, "the gap rose at t = {t}");
assert!((0.0..=1.0).contains(&gap));
previous = gap;
}
let ratio = bcs_gap_equation(0.99 * tc, tc).unwrap()
/ bcs_gap_equation(0.9999 * tc, tc).unwrap();
assert!((9.0..11.0).contains(&ratio), "the opening ratio is {ratio}, not near ten");
assert!(bcs_gap_equation(1.0, 0.0).is_err());
let weak = bcs_tc_from_coupling(0.2, 400.0).unwrap();
let strong = bcs_tc_from_coupling(0.4, 400.0).unwrap();
assert!(strong > 10.0 * weak, "doubling the coupling gave {strong} against {weak}");
assert!(close(
bcs_tc_from_coupling(0.3, 800.0).unwrap(),
2.0 * bcs_tc_from_coupling(0.3, 400.0).unwrap(),
1e-9
));
assert!(bcs_tc_from_coupling(0.0, 400.0).is_err());
assert!(close(josephson_current(1e-6, 0.0), 0.0, 1e-18));
assert!(relative(josephson_current(1e-6, std::f64::consts::FRAC_PI_2), 1e-6) < 1e-12);
assert!(close(josephson_current(1e-6, std::f64::consts::PI), 0.0, 1e-18));
assert!(
relative(josephson_frequency(1e-6), 483.597_848_4e6) < 1e-6,
"the frequency is {} hertz per microvolt",
josephson_frequency(1e-6)
);
assert!(close(josephson_frequency(0.0), 0.0, 1e-12));
}
#[test]
fn every_state_of_a_disordered_chain_is_localised_and_more_so_at_stronger_disorder() {
let mut rng = Rng::new(0x_5011_0001);
let mut previous = f64::INFINITY;
for disorder in [0.5f64, 1.0, 2.0, 4.0, 8.0] {
let length = anderson_localization_1d(4000, disorder, 0.0, 20, &mut rng).unwrap();
assert!(length > 0.0 && length.is_finite(), "the length is {length}");
assert!(
length < previous,
"raising the disorder to {disorder} lengthened the states to {length}"
);
previous = length;
}
assert!(previous < 2.0, "strong disorder should localise within a few sites: {previous}");
let mut fresh = Rng::new(0x_5011_0002);
for energy in [0.0f64, 0.5] {
let weak = anderson_localization_1d(150_000, 0.4, energy, 24, &mut fresh).unwrap();
let weaker = anderson_localization_1d(150_000, 0.2, energy, 24, &mut fresh).unwrap();
let ratio = weaker / weak;
assert!(
(3.6..4.4).contains(&ratio),
"at E = {energy}, halving the disorder changed the length by {ratio}"
);
assert!(
(100.0..20_000.0).contains(&weaker),
"the weak-disorder length is {weaker} sites"
);
}
assert!(anderson_localization_1d(5, 1.0, 0.0, 10, &mut rng).is_err());
assert!(anderson_localization_1d(100, 0.0, 0.0, 10, &mut rng).is_err());
assert!(anderson_localization_1d(100, 1.0, 0.0, 0, &mut rng).is_err());
}
}