#![allow(dead_code)]
struct SmRng {
state: u64,
}
impl SmRng {
fn new(seed: u64) -> Self {
Self { state: seed.max(1) }
}
fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.state
}
fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EnsembleKind {
Canonical,
GrandCanonical,
Microcanonical,
}
#[derive(Debug, Clone)]
pub struct PartitionFunction {
pub kind: EnsembleKind,
pub energy_levels: Vec<f64>,
pub degeneracies: Vec<f64>,
pub temperature: f64,
pub chemical_potential: f64,
pub particle_numbers: Vec<f64>,
}
impl PartitionFunction {
pub fn canonical(energy_levels: Vec<f64>, degeneracies: Vec<f64>, temperature: f64) -> Self {
Self {
kind: EnsembleKind::Canonical,
energy_levels,
degeneracies,
temperature,
chemical_potential: 0.0,
particle_numbers: vec![],
}
}
pub fn grand_canonical(
energy_levels: Vec<f64>,
degeneracies: Vec<f64>,
particle_numbers: Vec<f64>,
temperature: f64,
chemical_potential: f64,
) -> Self {
Self {
kind: EnsembleKind::GrandCanonical,
energy_levels,
degeneracies,
temperature,
chemical_potential,
particle_numbers,
}
}
pub fn microcanonical(energy_levels: Vec<f64>, degeneracies: Vec<f64>) -> Self {
Self {
kind: EnsembleKind::Microcanonical,
energy_levels,
degeneracies,
temperature: 0.0,
chemical_potential: 0.0,
particle_numbers: vec![],
}
}
pub fn canonical_z(&self) -> f64 {
if self.temperature <= 0.0 {
return 0.0;
}
let beta = 1.0 / self.temperature;
self.energy_levels
.iter()
.zip(self.degeneracies.iter())
.map(|(&e, &g)| g * (-beta * e).exp())
.sum()
}
pub fn grand_canonical_z(&self) -> f64 {
if self.temperature <= 0.0 {
return 0.0;
}
let beta = 1.0 / self.temperature;
let mu = self.chemical_potential;
let n_iter = self
.particle_numbers
.iter()
.chain(std::iter::repeat(&0.0_f64));
self.energy_levels
.iter()
.zip(self.degeneracies.iter())
.zip(n_iter)
.map(|((&e, &g), &n)| g * (-beta * (e - mu * n)).exp())
.sum()
}
pub fn density_of_states(&self, target_energy: f64, tolerance: f64) -> f64 {
self.energy_levels
.iter()
.zip(self.degeneracies.iter())
.filter(|&(&e, _)| (e - target_energy).abs() <= tolerance)
.map(|(_, &g)| g)
.sum()
}
pub fn free_energy(&self) -> f64 {
let z = self.canonical_z();
if z <= 0.0 {
return f64::INFINITY;
}
-self.temperature * z.ln()
}
pub fn average_energy(&self) -> f64 {
if self.temperature <= 0.0 {
return 0.0;
}
let beta = 1.0 / self.temperature;
let z = self.canonical_z();
if z <= 0.0 {
return 0.0;
}
let num: f64 = self
.energy_levels
.iter()
.zip(self.degeneracies.iter())
.map(|(&e, &g)| g * e * (-beta * e).exp())
.sum();
num / z
}
pub fn entropy(&self) -> f64 {
if self.temperature <= 0.0 {
return 0.0;
}
(self.average_energy() - self.free_energy()) / self.temperature
}
pub fn heat_capacity(&self) -> f64 {
if self.temperature <= 0.0 {
return 0.0;
}
let beta = 1.0 / self.temperature;
let z = self.canonical_z();
if z <= 0.0 {
return 0.0;
}
let e_avg = self.average_energy();
let e2_avg: f64 = self
.energy_levels
.iter()
.zip(self.degeneracies.iter())
.map(|(&e, &g)| g * e * e * (-beta * e).exp())
.sum::<f64>()
/ z;
(e2_avg - e_avg * e_avg) / (self.temperature * self.temperature)
}
}
#[derive(Debug, Clone)]
pub struct BoltzmannDistribution {
pub energies: Vec<f64>,
pub degeneracies: Vec<f64>,
pub temperature: f64,
}
impl BoltzmannDistribution {
pub fn new(energies: Vec<f64>, degeneracies: Vec<f64>, temperature: f64) -> Self {
Self {
energies,
degeneracies,
temperature,
}
}
pub fn uniform(energies: Vec<f64>, temperature: f64) -> Self {
let n = energies.len();
Self::new(energies, vec![1.0; n], temperature)
}
pub fn weight(&self, i: usize) -> f64 {
if self.temperature <= 0.0 {
return 0.0;
}
let beta = 1.0 / self.temperature;
self.degeneracies[i] * (-beta * self.energies[i]).exp()
}
pub fn partition_function(&self) -> f64 {
(0..self.energies.len()).map(|i| self.weight(i)).sum()
}
pub fn probability(&self, i: usize) -> f64 {
let z = self.partition_function();
if z <= 0.0 {
return 0.0;
}
self.weight(i) / z
}
pub fn probabilities(&self) -> Vec<f64> {
let z = self.partition_function();
if z <= 0.0 {
return vec![0.0; self.energies.len()];
}
(0..self.energies.len())
.map(|i| self.weight(i) / z)
.collect()
}
pub fn entropy(&self) -> f64 {
self.probabilities()
.iter()
.filter(|&&p| p > 0.0)
.map(|&p| -p * p.ln())
.sum()
}
pub fn free_energy(&self) -> f64 {
let z = self.partition_function();
if z <= 0.0 {
return f64::INFINITY;
}
-self.temperature * z.ln()
}
pub fn mean_energy(&self) -> f64 {
let probs = self.probabilities();
probs
.iter()
.zip(self.energies.iter())
.map(|(&p, &e)| p * e)
.sum()
}
}
#[derive(Debug, Clone)]
pub struct IsingModel {
pub dim: usize,
pub lx: usize,
pub ly: usize,
pub j_coupling: f64,
pub field: f64,
pub spins: Vec<i8>,
}
impl IsingModel {
pub fn new_1d(n: usize, j_coupling: f64, field: f64) -> Self {
Self {
dim: 1,
lx: n,
ly: 1,
j_coupling,
field,
spins: vec![1i8; n],
}
}
pub fn new_2d(lx: usize, ly: usize, j_coupling: f64, field: f64) -> Self {
Self {
dim: 2,
lx,
ly,
j_coupling,
field,
spins: vec![1i8; lx * ly],
}
}
pub fn randomise(&mut self, seed: u64) {
let mut rng = SmRng::new(seed);
for s in self.spins.iter_mut() {
*s = if rng.next_f64() < 0.5 { 1 } else { -1 };
}
}
pub fn energy(&self) -> f64 {
let mut e = 0.0_f64;
let n = self.spins.len();
if self.dim == 1 {
for i in 0..n {
let j = (i + 1) % n;
e -= self.j_coupling * (self.spins[i] as f64) * (self.spins[j] as f64);
e -= self.field * self.spins[i] as f64;
}
} else {
for row in 0..self.ly {
for col in 0..self.lx {
let idx = row * self.lx + col;
let right = row * self.lx + (col + 1) % self.lx;
let down = ((row + 1) % self.ly) * self.lx + col;
e -= self.j_coupling
* (self.spins[idx] as f64)
* (self.spins[right] as f64 + self.spins[down] as f64);
e -= self.field * self.spins[idx] as f64;
}
}
}
e
}
pub fn magnetisation(&self) -> f64 {
let total: i64 = self.spins.iter().map(|&s| s as i64).sum();
total as f64 / self.spins.len() as f64
}
fn delta_energy(&self, idx: usize) -> f64 {
let s = self.spins[idx] as f64;
let neighbour_sum: f64 = if self.dim == 1 {
let n = self.spins.len();
let left = (idx + n - 1) % n;
let right = (idx + 1) % n;
self.spins[left] as f64 + self.spins[right] as f64
} else {
let row = idx / self.lx;
let col = idx % self.lx;
let left = row * self.lx + (col + self.lx - 1) % self.lx;
let right = row * self.lx + (col + 1) % self.lx;
let up = ((row + self.ly - 1) % self.ly) * self.lx + col;
let down = ((row + 1) % self.ly) * self.lx + col;
self.spins[left] as f64
+ self.spins[right] as f64
+ self.spins[up] as f64
+ self.spins[down] as f64
};
2.0 * s * (self.j_coupling * neighbour_sum + self.field)
}
pub fn metropolis_sweep(&mut self, n_sweeps: usize, temperature: f64, seed: u64) {
let mut rng = SmRng::new(seed);
let n = self.spins.len();
for _ in 0..n_sweeps {
for _attempt in 0..n {
let idx = (rng.next_u64() as usize) % n;
let de = self.delta_energy(idx);
if de <= 0.0 || rng.next_f64() < (-de / temperature).exp() {
self.spins[idx] = -self.spins[idx];
}
}
}
}
pub fn susceptibility(
&mut self,
temperature: f64,
n_eq: usize,
n_samples: usize,
seed: u64,
) -> f64 {
self.metropolis_sweep(n_eq, temperature, seed);
let n = self.spins.len();
let mut m_sum = 0.0_f64;
let mut m2_sum = 0.0_f64;
let mut rng = SmRng::new(seed.wrapping_add(1));
for _ in 0..n_samples {
for _attempt in 0..n {
let idx = (rng.next_u64() as usize) % n;
let de = self.delta_energy(idx);
if de <= 0.0 || rng.next_f64() < (-de / temperature).exp() {
self.spins[idx] = -self.spins[idx];
}
}
let m = self.magnetisation();
m_sum += m;
m2_sum += m * m;
}
let m_avg = m_sum / n_samples as f64;
let m2_avg = m2_sum / n_samples as f64;
(m2_avg - m_avg * m_avg) * n as f64 / temperature
}
pub fn critical_temperature_1d_approx(&self) -> f64 {
2.0 * self.j_coupling.abs()
}
pub fn mean_field_tc_2d(&self) -> f64 {
4.0 * self.j_coupling
}
pub fn onsager_tc(&self) -> f64 {
2.0 * self.j_coupling / (1.0 + 2.0_f64.sqrt()).ln()
}
}
#[derive(Debug, Clone)]
pub struct PhaseTransition {
pub a0: f64,
pub tc: f64,
pub b: f64,
pub c: f64,
pub temperature: f64,
}
impl PhaseTransition {
pub fn new(a0: f64, tc: f64, b: f64, c: f64, temperature: f64) -> Self {
Self {
a0,
tc,
b,
c,
temperature,
}
}
pub fn a_coeff(&self) -> f64 {
self.a0 * (self.temperature - self.tc)
}
pub fn order_parameter(&self) -> f64 {
let a = self.a_coeff();
if a >= 0.0 {
return 0.0;
}
if self.b > 0.0 {
(-a / self.b).sqrt()
} else {
0.0
}
}
pub fn free_energy_density(&self, phi: f64) -> f64 {
let a = self.a_coeff();
a * phi * phi / 2.0 + self.b * phi.powi(4) / 4.0 + self.c * phi.powi(6) / 6.0
}
pub fn reduced_temperature(&self) -> f64 {
if self.tc == 0.0 {
return 0.0;
}
(self.temperature - self.tc) / self.tc
}
pub fn mean_field_beta_exponent(&self) -> f64 {
0.5
}
pub fn susceptibility(&self) -> f64 {
let a = self.a_coeff().abs();
if a < 1e-15 {
return f64::INFINITY;
}
1.0 / (2.0 * a)
}
}
#[derive(Debug, Clone)]
pub struct FluctuationDissipation {
pub temperature: f64,
}
impl FluctuationDissipation {
pub fn new(temperature: f64) -> Self {
Self { temperature }
}
pub fn einstein_relation(&self, mobility: f64) -> f64 {
mobility * self.temperature
}
pub fn mobility_from_diffusivity(&self, diffusivity: f64) -> f64 {
if self.temperature <= 0.0 {
return 0.0;
}
diffusivity / self.temperature
}
pub fn noise_power_spectral_density(&self, resistance: f64) -> f64 {
4.0 * self.temperature * resistance
}
pub fn response_function_imaginary(&self, omega: f64, omega0: f64, gamma: f64) -> f64 {
let denom = (omega0 * omega0 - omega * omega).powi(2) + gamma * gamma * omega * omega;
if denom < 1e-30 {
return 0.0;
}
gamma * omega / denom
}
pub fn response_function_real(&self, omega: f64, omega0: f64, gamma: f64) -> f64 {
let denom = (omega0 * omega0 - omega * omega).powi(2) + gamma * gamma * omega * omega;
if denom < 1e-30 {
return 0.0;
}
(omega0 * omega0 - omega * omega) / denom
}
pub fn fluctuation_spectrum(&self, omega: f64, omega0: f64, gamma: f64) -> f64 {
if omega.abs() < 1e-30 {
return 0.0;
}
2.0 * self.temperature * self.response_function_imaginary(omega, omega0, gamma) / omega
}
}
#[derive(Debug, Clone)]
pub struct GreenKubo {
pub dt: f64,
pub temperature: f64,
pub volume: f64,
}
impl GreenKubo {
pub fn new(dt: f64, temperature: f64, volume: f64) -> Self {
Self {
dt,
temperature,
volume,
}
}
pub fn autocorrelation(signal: &[f64]) -> Vec<f64> {
let n = signal.len();
if n == 0 {
return vec![];
}
let mean: f64 = signal.iter().sum::<f64>() / n as f64;
let centered: Vec<f64> = signal.iter().map(|&x| x - mean).collect();
let norm: f64 = centered.iter().map(|&x| x * x).sum();
if norm < 1e-30 {
return vec![0.0; n];
}
(0..n)
.map(|lag| {
let count = n - lag;
let sum: f64 = (0..count).map(|i| centered[i] * centered[i + lag]).sum();
sum / norm
})
.collect()
}
pub fn diffusivity_from_vacf(&self, vacf: &[f64]) -> f64 {
let n = vacf.len();
if n == 0 {
return 0.0;
}
let integral: f64 = (0..n - 1)
.map(|i| 0.5 * (vacf[i] + vacf[i + 1]) * self.dt)
.sum();
integral / 3.0
}
pub fn shear_viscosity(&self, stress_acf: &[f64]) -> f64 {
if self.temperature <= 0.0 || self.volume <= 0.0 {
return 0.0;
}
let n = stress_acf.len();
if n == 0 {
return 0.0;
}
let integral: f64 = (0..n - 1)
.map(|i| 0.5 * (stress_acf[i] + stress_acf[i + 1]) * self.dt)
.sum();
self.volume / self.temperature * integral
}
pub fn electrical_conductivity(&self, current_acf: &[f64]) -> f64 {
if self.temperature <= 0.0 || self.volume <= 0.0 {
return 0.0;
}
let n = current_acf.len();
if n == 0 {
return 0.0;
}
let integral: f64 = (0..n - 1)
.map(|i| 0.5 * (current_acf[i] + current_acf[i + 1]) * self.dt)
.sum();
self.volume / (3.0 * self.temperature) * integral
}
pub fn correlation_time(acf: &[f64], dt: f64) -> f64 {
if acf.is_empty() || acf[0].abs() < 1e-30 {
return 0.0;
}
let integral: f64 = (0..acf.len() - 1)
.map(|i| 0.5 * (acf[i] + acf[i + 1]) * dt)
.sum();
integral / acf[0]
}
}
#[derive(Debug, Clone)]
pub struct EquationOfState {
pub r_gas: f64,
pub temperature: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EosModel {
VanDerWaals,
PengRobinson,
RedlichKwong,
}
impl EquationOfState {
pub fn new(r_gas: f64, temperature: f64) -> Self {
Self { r_gas, temperature }
}
pub fn van_der_waals_pressure(&self, molar_volume: f64, a: f64, b: f64) -> f64 {
if molar_volume <= b {
return f64::INFINITY;
}
self.r_gas * self.temperature / (molar_volume - b) - a / (molar_volume * molar_volume)
}
pub fn vdw_critical_constants(a: f64, b: f64, r_gas: f64) -> (f64, f64, f64) {
let tc = 8.0 * a / (27.0 * r_gas * b);
let pc = a / (27.0 * b * b);
let vc = 3.0 * b;
(tc, pc, vc)
}
#[allow(clippy::too_many_arguments)]
pub fn peng_robinson_pressure(
&self,
molar_volume: f64,
a_c: f64,
b: f64,
kappa: f64,
tc: f64,
) -> f64 {
if molar_volume <= b || tc <= 0.0 {
return f64::INFINITY;
}
let tr = self.temperature / tc;
let alpha = (1.0 + kappa * (1.0 - tr.sqrt())).powi(2);
let a_t = a_c * alpha;
self.r_gas * self.temperature / (molar_volume - b)
- a_t / (molar_volume * (molar_volume + b) + b * (molar_volume - b))
}
pub fn redlich_kwong_pressure(&self, molar_volume: f64, a: f64, b: f64) -> f64 {
if molar_volume <= b || self.temperature <= 0.0 {
return f64::INFINITY;
}
self.r_gas * self.temperature / (molar_volume - b)
- a / (self.temperature.sqrt() * molar_volume * (molar_volume + b))
}
pub fn compressibility_factor(&self, pressure: f64, molar_volume: f64) -> f64 {
if self.r_gas <= 0.0 || self.temperature <= 0.0 {
return 0.0;
}
pressure * molar_volume / (self.r_gas * self.temperature)
}
pub fn boyle_temperature_vdw(a: f64, b: f64, r_gas: f64) -> f64 {
a / (r_gas * b)
}
}
pub fn fermi_dirac(energy: f64, mu: f64, temperature: f64) -> f64 {
if temperature <= 0.0 {
return if energy < mu { 1.0 } else { 0.0 };
}
1.0 / (((energy - mu) / temperature).exp() + 1.0)
}
pub fn bose_einstein(energy: f64, mu: f64, temperature: f64) -> f64 {
if temperature <= 0.0 {
return 0.0;
}
let x = (energy - mu) / temperature;
if x <= 0.0 {
return 0.0;
}
let denom = x.exp() - 1.0;
if denom < 1e-30 { 0.0 } else { 1.0 / denom }
}
pub fn stefan_boltzmann_power(temperature: f64) -> f64 {
const SIGMA: f64 = 5.670_374_419e-8;
SIGMA * temperature.powi(4)
}
pub fn wien_peak_wavelength(temperature: f64) -> f64 {
const B: f64 = 2.897_771_955e-3;
if temperature <= 0.0 {
return f64::INFINITY;
}
B / temperature
}
pub fn planck_spectral_radiance(wavelength: f64, temperature: f64) -> f64 {
const H: f64 = 6.626_070_15e-34;
const C: f64 = 2.997_924_58e8;
const KB: f64 = 1.380_649e-23;
if wavelength <= 0.0 || temperature <= 0.0 {
return 0.0;
}
let exponent = H * C / (wavelength * KB * temperature);
if exponent > 700.0 {
return 0.0;
}
2.0 * H * C * C / (wavelength.powi(5) * (exponent.exp() - 1.0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canonical_z_two_level() {
let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 1.0);
let z = pf.canonical_z();
assert!((z - (1.0 + (-1.0_f64).exp())).abs() < 1e-12);
}
#[test]
fn test_canonical_z_zero_temp() {
let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 0.0);
assert_eq!(pf.canonical_z(), 0.0);
}
#[test]
fn test_free_energy_two_level() {
let t = 2.0;
let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], t);
let z = pf.canonical_z();
let f_expected = -t * z.ln();
assert!((pf.free_energy() - f_expected).abs() < 1e-12);
}
#[test]
fn test_average_energy_zero_temp() {
let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 0.0);
assert_eq!(pf.average_energy(), 0.0);
}
#[test]
fn test_average_energy_high_temp() {
let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 1e6);
let e_avg = pf.average_energy();
assert!((e_avg - 0.5).abs() < 1e-3);
}
#[test]
fn test_entropy_nonnegative() {
let pf = PartitionFunction::canonical(vec![0.0, 1.0, 2.0], vec![1.0, 2.0, 1.0], 1.5);
assert!(pf.entropy() >= 0.0);
}
#[test]
fn test_heat_capacity_positive() {
let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 1.0);
assert!(pf.heat_capacity() > 0.0);
}
#[test]
fn test_grand_canonical_z() {
let pf = PartitionFunction::grand_canonical(
vec![0.0, 1.0],
vec![1.0, 1.0],
vec![0.0, 1.0],
1.0,
0.5,
);
let z = pf.grand_canonical_z();
assert!(z > 0.0);
}
#[test]
fn test_density_of_states() {
let pf = PartitionFunction::microcanonical(vec![0.0, 1.0, 2.0], vec![1.0, 3.0, 1.0]);
let omega = pf.density_of_states(1.0, 0.1);
assert!((omega - 3.0).abs() < 1e-12);
}
#[test]
fn test_boltzmann_probabilities_sum_to_one() {
let bd = BoltzmannDistribution::uniform(vec![0.0, 1.0, 2.0, 3.0], 1.0);
let sum: f64 = bd.probabilities().iter().sum();
assert!((sum - 1.0).abs() < 1e-12);
}
#[test]
fn test_boltzmann_entropy_nonneg() {
let bd = BoltzmannDistribution::uniform(vec![0.0, 1.0, 2.0], 1.0);
assert!(bd.entropy() >= 0.0);
}
#[test]
fn test_boltzmann_free_energy() {
let bd = BoltzmannDistribution::uniform(vec![0.0, 0.5, 1.0], 0.5);
let f = bd.free_energy();
assert!(f.is_finite());
}
#[test]
fn test_boltzmann_mean_energy() {
let bd = BoltzmannDistribution::uniform(vec![0.0, 1.0], 1e6);
assert!((bd.mean_energy() - 0.5).abs() < 1e-3);
}
#[test]
fn test_boltzmann_zero_temp() {
let bd = BoltzmannDistribution::uniform(vec![0.0, 1.0], 0.0);
assert_eq!(bd.partition_function(), 0.0);
}
#[test]
fn test_ising_1d_energy_all_up() {
let model = IsingModel::new_1d(4, 1.0, 0.0);
assert!((model.energy() - (-4.0)).abs() < 1e-12);
}
#[test]
fn test_ising_magnetisation_all_up() {
let model = IsingModel::new_1d(4, 1.0, 0.0);
assert!((model.magnetisation() - 1.0).abs() < 1e-12);
}
#[test]
fn test_ising_magnetisation_alternating() {
let mut model = IsingModel::new_1d(4, 1.0, 0.0);
for i in 0..4 {
model.spins[i] = if i % 2 == 0 { 1 } else { -1 };
}
assert!(model.magnetisation().abs() < 1e-12);
}
#[test]
fn test_ising_2d_energy_all_up() {
let model = IsingModel::new_2d(2, 2, 1.0, 0.0);
assert!(model.energy() < 0.0);
}
#[test]
fn test_ising_randomise_changes_spins() {
let mut model = IsingModel::new_1d(10, 1.0, 0.0);
model.randomise(42);
let all_up = model.spins.iter().all(|&s| s == 1);
let all_dn = model.spins.iter().all(|&s| s == -1);
assert!(!(all_up && all_dn));
}
#[test]
fn test_ising_metropolis_runs() {
let mut model = IsingModel::new_1d(8, 1.0, 0.0);
model.randomise(1);
model.metropolis_sweep(10, 2.0, 99);
assert!(model.magnetisation().abs() <= 1.0);
}
#[test]
fn test_ising_onsager_tc() {
let model = IsingModel::new_2d(8, 8, 1.0, 0.0);
let tc = model.onsager_tc();
assert!((tc - 2.269).abs() < 0.01);
}
#[test]
fn test_ising_susceptibility_positive() {
let mut model = IsingModel::new_1d(8, 1.0, 0.0);
model.randomise(7);
let chi = model.susceptibility(2.5, 20, 50, 11);
assert!(chi >= 0.0);
}
#[test]
fn test_phase_transition_order_param_above_tc() {
let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.0, 3.0);
assert_eq!(pt.order_parameter(), 0.0);
}
#[test]
fn test_phase_transition_order_param_below_tc() {
let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.0, 1.0);
let phi = pt.order_parameter();
assert!(phi > 0.0);
}
#[test]
fn test_phase_transition_free_energy_density() {
let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.5, 1.0);
let f0 = pt.free_energy_density(0.0);
let phi_eq = pt.order_parameter();
let f_eq = pt.free_energy_density(phi_eq);
assert!(f_eq <= f0 + 1e-10);
}
#[test]
fn test_phase_transition_reduced_temperature() {
let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.0, 3.0);
assert!((pt.reduced_temperature() - 0.5).abs() < 1e-12);
}
#[test]
fn test_phase_transition_susceptibility_diverges_near_tc() {
let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.0, 2.0 + 1e-8);
assert!(pt.susceptibility() > 1e5);
}
#[test]
fn test_einstein_relation() {
let fd = FluctuationDissipation::new(1.0);
assert!((fd.einstein_relation(0.5) - 0.5).abs() < 1e-12);
}
#[test]
fn test_mobility_from_diffusivity() {
let fd = FluctuationDissipation::new(2.0);
assert!((fd.mobility_from_diffusivity(4.0) - 2.0).abs() < 1e-12);
}
#[test]
fn test_noise_power_spectral_density() {
let fd = FluctuationDissipation::new(300.0);
let s = fd.noise_power_spectral_density(50.0);
assert!((s - 4.0 * 300.0 * 50.0).abs() < 1e-6);
}
#[test]
fn test_response_function_imaginary_at_resonance() {
let fd = FluctuationDissipation::new(1.0);
let chi_im = fd.response_function_imaginary(1.0, 1.0, 0.1);
assert!(chi_im > 0.0);
}
#[test]
fn test_fluctuation_spectrum_zero_omega() {
let fd = FluctuationDissipation::new(1.0);
assert_eq!(fd.fluctuation_spectrum(0.0, 1.0, 0.1), 0.0);
}
#[test]
fn test_autocorrelation_constant_signal() {
let signal = vec![1.0; 10];
let acf = GreenKubo::autocorrelation(&signal);
assert_eq!(acf.len(), 10);
for v in &acf {
assert!(v.abs() < 1e-12);
}
}
#[test]
fn test_autocorrelation_lag_zero_is_one() {
let signal: Vec<f64> = (0..20).map(|i| (i as f64 * 0.3).sin()).collect();
let acf = GreenKubo::autocorrelation(&signal);
assert!(!acf.is_empty());
assert!((acf[0] - 1.0).abs() < 1e-10);
}
#[test]
fn test_diffusivity_from_vacf() {
let gk = GreenKubo::new(0.01, 1.0, 1.0);
let tau = 1.0;
let n = 200;
let vacf: Vec<f64> = (0..n).map(|i| (-(i as f64) * 0.01 / tau).exp()).collect();
let d = gk.diffusivity_from_vacf(&vacf);
assert!((d - tau / 3.0).abs() < 0.05);
}
#[test]
fn test_shear_viscosity_zero_temp() {
let gk = GreenKubo::new(0.01, 0.0, 1.0);
assert_eq!(gk.shear_viscosity(&[1.0, 0.5, 0.1]), 0.0);
}
#[test]
fn test_correlation_time() {
let tau = 2.0;
let dt = 0.1;
let n = 100;
let acf: Vec<f64> = (0..n).map(|i| (-(i as f64) * dt / tau).exp()).collect();
let ct = GreenKubo::correlation_time(&acf, dt);
assert!((ct - tau).abs() < 0.5);
}
#[test]
fn test_vdw_ideal_limit() {
let eos = EquationOfState::new(8.314, 300.0);
let v = 0.025; let p = eos.van_der_waals_pressure(v, 0.0, 0.0);
assert!((p - 8.314 * 300.0 / v).abs() < 1e-6);
}
#[test]
fn test_vdw_critical_constants() {
let (tc, pc, vc) = EquationOfState::vdw_critical_constants(0.364, 4.27e-5, 8.314);
assert!(tc > 0.0 && pc > 0.0 && vc > 0.0);
}
#[test]
fn test_peng_robinson_pressure_finite() {
let eos = EquationOfState::new(8.314, 300.0);
let p = eos.peng_robinson_pressure(0.001, 0.4, 2.5e-5, 0.37, 190.0);
assert!(p.is_finite());
}
#[test]
fn test_redlich_kwong_pressure_finite() {
let eos = EquationOfState::new(8.314, 400.0);
let p = eos.redlich_kwong_pressure(0.002, 1.0, 2.6e-5);
assert!(p.is_finite() && p > 0.0);
}
#[test]
fn test_compressibility_factor_ideal_gas() {
let eos = EquationOfState::new(8.314, 300.0);
let v = 0.025;
let p = eos.van_der_waals_pressure(v, 0.0, 0.0);
let z = eos.compressibility_factor(p, v);
assert!((z - 1.0).abs() < 1e-10);
}
#[test]
fn test_boyle_temperature_vdw() {
let t_b = EquationOfState::boyle_temperature_vdw(0.364, 4.27e-5, 8.314);
assert!(t_b > 0.0);
}
#[test]
fn test_fermi_dirac_at_mu() {
assert!((fermi_dirac(1.0, 1.0, 1.0) - 0.5).abs() < 1e-12);
}
#[test]
fn test_fermi_dirac_below_mu() {
assert!(fermi_dirac(0.0, 1.0, 1.0) > 0.5);
}
#[test]
fn test_fermi_dirac_zero_temp_below_mu() {
assert_eq!(fermi_dirac(0.5, 1.0, 0.0), 1.0);
}
#[test]
fn test_fermi_dirac_zero_temp_above_mu() {
assert_eq!(fermi_dirac(1.5, 1.0, 0.0), 0.0);
}
#[test]
fn test_bose_einstein_positive() {
let n = bose_einstein(1.0, 0.5, 1.0);
assert!(n > 0.0);
}
#[test]
fn test_bose_einstein_at_zero_argument() {
assert_eq!(bose_einstein(1.0, 1.0, 1.0), 0.0);
}
#[test]
fn test_planck_spectral_radiance_positive() {
let b = planck_spectral_radiance(500e-9, 5778.0);
assert!(b > 0.0);
}
#[test]
fn test_wien_peak_wavelength() {
let lam = wien_peak_wavelength(5778.0);
assert!((lam - 5.02e-7).abs() < 1e-8);
}
#[test]
fn test_stefan_boltzmann() {
let p = stefan_boltzmann_power(1.0);
assert!((p - 5.670374419e-8).abs() < 1e-15);
}
#[allow(clippy::too_many_arguments)]
#[test]
fn test_partition_function_degeneracy_scaling() {
let pf1 = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 1.0);
let pf2 = PartitionFunction::canonical(vec![0.0, 1.0], vec![2.0, 2.0], 1.0);
let diff = (pf1.average_energy() - pf2.average_energy()).abs();
assert!(diff < 1e-12);
}
#[test]
fn test_ising_delta_energy_flip_all_up() {
let model = IsingModel::new_1d(4, 1.0, 0.0);
let de = model.delta_energy(0);
assert!((de - 4.0).abs() < 1e-12);
}
#[test]
fn test_phase_transition_a_coeff() {
let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.0, 3.0);
assert!((pt.a_coeff() - 1.0).abs() < 1e-12);
}
#[test]
fn test_gk_autocorrelation_empty() {
let acf = GreenKubo::autocorrelation(&[]);
assert!(acf.is_empty());
}
#[test]
fn test_electrical_conductivity_zero_volume() {
let gk = GreenKubo::new(0.01, 1.0, 0.0);
assert_eq!(gk.electrical_conductivity(&[1.0, 0.5]), 0.0);
}
#[test]
fn test_vdw_pressure_below_b_returns_inf() {
let eos = EquationOfState::new(8.314, 300.0);
let p = eos.van_der_waals_pressure(1e-6, 0.0, 0.01);
assert_eq!(p, f64::INFINITY);
}
#[test]
fn test_mean_field_tc_2d() {
let model = IsingModel::new_2d(4, 4, 1.0, 0.0);
assert!((model.mean_field_tc_2d() - 4.0).abs() < 1e-12);
}
}