use std::f64::consts::PI;
pub struct StatMech;
impl StatMech {
pub const K_B: f64 = 1.380_649e-23;
pub const N_A: f64 = 6.022_140_76e23;
pub const H: f64 = 6.626_070_15e-34;
pub fn boltzmann_factor(energy_j: f64, temperature_k: f64) -> f64 {
if temperature_k <= 0.0 {
return 0.0;
}
(-energy_j / (Self::K_B * temperature_k)).exp()
}
pub fn partition_function(levels: &[(f64, f64)], temperature_k: f64) -> f64 {
if temperature_k <= 0.0 {
return 0.0;
}
levels
.iter()
.map(|(e, g)| g * Self::boltzmann_factor(*e, temperature_k))
.sum()
}
pub fn boltzmann_probabilities(levels: &[(f64, f64)], temperature_k: f64) -> Vec<f64> {
let z = Self::partition_function(levels, temperature_k);
if z < f64::EPSILON {
return vec![0.0; levels.len()];
}
levels
.iter()
.map(|(e, g)| g * Self::boltzmann_factor(*e, temperature_k) / z)
.collect()
}
pub fn entropy_from_microstates(microstates: f64) -> f64 {
if microstates < 1.0 {
return 0.0;
}
Self::K_B * microstates.ln()
}
pub fn gibbs_entropy(probabilities: &[f64]) -> f64 {
-Self::K_B
* probabilities
.iter()
.filter(|&&p| p > 0.0)
.map(|&p| p * p.ln())
.sum::<f64>()
}
pub fn energy_per_dof(temperature_k: f64) -> f64 {
0.5 * Self::K_B * temperature_k
}
pub fn total_average_energy(degrees_of_freedom: u32, temperature_k: f64) -> f64 {
degrees_of_freedom as f64 * Self::energy_per_dof(temperature_k)
}
pub fn fermi_dirac(energy_j: f64, chemical_potential_j: f64, temperature_k: f64) -> f64 {
if temperature_k <= 0.0 {
return if energy_j <= chemical_potential_j {
1.0
} else {
0.0
};
}
let exponent = (energy_j - chemical_potential_j) / (Self::K_B * temperature_k);
1.0 / (exponent.exp() + 1.0)
}
pub fn bose_einstein(
energy_j: f64,
chemical_potential_j: f64,
temperature_k: f64,
) -> Option<f64> {
if temperature_k <= 0.0 {
return None;
}
let exponent = (energy_j - chemical_potential_j) / (Self::K_B * temperature_k);
let denom = exponent.exp() - 1.0;
if denom <= 0.0 {
return None;
}
Some(1.0 / denom)
}
pub fn mean_free_path(diameter_m: f64, number_density: f64) -> Option<f64> {
if diameter_m <= 0.0 || number_density <= 0.0 {
return None;
}
let cross_section = PI * diameter_m * diameter_m;
Some(1.0 / (2.0_f64.sqrt() * cross_section * number_density))
}
pub fn mean_free_path_from_pressure(
diameter_m: f64,
pressure_pa: f64,
temperature_k: f64,
) -> Option<f64> {
if diameter_m <= 0.0 || pressure_pa <= 0.0 || temperature_k <= 0.0 {
return None;
}
let cross_section = PI * diameter_m * diameter_m;
Some(Self::K_B * temperature_k / (2.0_f64.sqrt() * cross_section * pressure_pa))
}
}
#[derive(Debug, Clone)]
pub struct MaxwellBoltzmannDist {
pub mass_kg: f64,
pub temperature_k: f64,
}
impl MaxwellBoltzmannDist {
pub fn new(mass_kg: f64, temperature_k: f64) -> Self {
assert!(mass_kg > 0.0, "mass must be positive");
assert!(temperature_k > 0.0, "temperature must be positive");
Self {
mass_kg,
temperature_k,
}
}
pub fn thermal_speed(&self) -> f64 {
(StatMech::K_B * self.temperature_k / self.mass_kg).sqrt()
}
pub fn pdf(&self, v: f64) -> f64 {
if v < 0.0 {
return 0.0;
}
let a = self.thermal_speed();
let a2 = a * a;
let prefactor = 4.0
* PI
* (self.mass_kg / (2.0 * PI * StatMech::K_B * self.temperature_k))
.powi(3)
.sqrt();
prefactor * v * v * (-v * v / (2.0 * a2)).exp()
}
pub fn most_probable_speed(&self) -> f64 {
let a = self.thermal_speed();
2.0_f64.sqrt() * a
}
pub fn mean_speed(&self) -> f64 {
let a = self.thermal_speed();
(8.0 / PI).sqrt() * a
}
pub fn rms_speed(&self) -> f64 {
let a = self.thermal_speed();
3.0_f64.sqrt() * a
}
pub fn cdf(&self, v: f64) -> f64 {
if v <= 0.0 {
return 0.0;
}
let a = self.thermal_speed();
let x = v / (2.0_f64.sqrt() * a);
erf(x) - (2.0 / PI).sqrt() * (v / a) * (-(v * v) / (2.0 * a * a)).exp()
}
}
fn erf(x: f64) -> f64 {
if x < 0.0 {
return -erf(-x);
}
let t = 1.0 / (1.0 + 0.3275911 * x);
let poly = t
* (0.254_829_592
+ t * (-0.284_496_736
+ t * (1.421_413_741 + t * (-1.453_152_027 + t * 1.061_405_429))));
1.0 - poly * (-x * x).exp()
}
#[cfg(test)]
mod tests {
use super::*;
const K_B: f64 = StatMech::K_B;
const MASS_N2: f64 = 0.028 / StatMech::N_A;
#[test]
fn test_boltzmann_factor_zero_energy() {
let bf = StatMech::boltzmann_factor(0.0, 300.0);
assert!((bf - 1.0).abs() < 1e-10);
}
#[test]
fn test_boltzmann_factor_infinite_temperature() {
let bf = StatMech::boltzmann_factor(1e-20, 1e12);
assert!((bf - 1.0).abs() < 1e-3);
}
#[test]
fn test_boltzmann_factor_negative_temperature() {
let bf = StatMech::boltzmann_factor(1e-21, -100.0);
assert_eq!(bf, 0.0);
}
#[test]
fn test_boltzmann_factor_large_energy() {
let bf = StatMech::boltzmann_factor(1.0e10, 1.0);
assert!(bf < 1e-100);
}
#[test]
fn test_partition_function_single_level() {
let levels = [(0.0_f64, 1.0_f64)]; let z = StatMech::partition_function(&levels, 300.0);
assert!((z - 1.0).abs() < 1e-10);
}
#[test]
fn test_partition_function_degenerate_ground_state() {
let levels = [(0.0_f64, 3.0_f64)]; let z = StatMech::partition_function(&levels, 300.0);
assert!((z - 3.0).abs() < 1e-10);
}
#[test]
fn test_partition_function_two_levels_low_temp() {
let e1 = K_B * 1e6; let levels = [(0.0_f64, 1.0_f64), (e1, 1.0_f64)];
let z = StatMech::partition_function(&levels, 300.0);
assert!((z - 1.0).abs() < 1e-6);
}
#[test]
fn test_partition_function_zero_temperature() {
let levels = [(0.0_f64, 1.0_f64), (1e-20_f64, 1.0_f64)];
let z = StatMech::partition_function(&levels, 0.0);
assert_eq!(z, 0.0);
}
#[test]
fn test_boltzmann_probabilities_sum_to_one() {
let levels = [(0.0_f64, 1.0_f64), (K_B * 100.0, 1.0), (K_B * 200.0, 1.0)];
let probs = StatMech::boltzmann_probabilities(&levels, 300.0);
let total: f64 = probs.iter().sum();
assert!((total - 1.0).abs() < 1e-10);
}
#[test]
fn test_boltzmann_probabilities_ground_state_most_probable() {
let levels = [
(0.0_f64, 1.0_f64),
(K_B * 1000.0, 1.0), ];
let probs = StatMech::boltzmann_probabilities(&levels, 300.0);
assert!(probs[0] > probs[1]);
}
#[test]
fn test_boltzmann_probabilities_equal_when_same_energy() {
let levels = [(0.0_f64, 1.0_f64), (0.0_f64, 1.0_f64)];
let probs = StatMech::boltzmann_probabilities(&levels, 300.0);
assert!((probs[0] - probs[1]).abs() < 1e-10);
}
#[test]
fn test_entropy_one_microstate_is_zero() {
let s = StatMech::entropy_from_microstates(1.0);
assert!((s - 0.0).abs() < 1e-40);
}
#[test]
fn test_entropy_increases_with_microstates() {
let s1 = StatMech::entropy_from_microstates(10.0);
let s2 = StatMech::entropy_from_microstates(100.0);
assert!(s2 > s1);
}
#[test]
fn test_entropy_formula_known_value() {
let omega = 100.0_f64;
let expected = K_B * omega.ln();
let computed = StatMech::entropy_from_microstates(omega);
assert!((computed - expected).abs() < 1e-40);
}
#[test]
fn test_entropy_below_one_microstate() {
let s = StatMech::entropy_from_microstates(0.5);
assert_eq!(s, 0.0);
}
#[test]
fn test_gibbs_entropy_uniform_distribution() {
let n = 4_usize;
let probs = vec![0.25; n];
let s = StatMech::gibbs_entropy(&probs);
let expected = K_B * (n as f64).ln();
assert!((s - expected).abs() < 1e-35);
}
#[test]
fn test_gibbs_entropy_certain_state() {
let probs = vec![1.0, 0.0, 0.0];
let s = StatMech::gibbs_entropy(&probs);
assert!(s.abs() < 1e-35);
}
#[test]
fn test_energy_per_dof_300k() {
let e = StatMech::energy_per_dof(300.0);
let expected = 0.5 * K_B * 300.0;
assert!((e - expected).abs() < 1e-35);
}
#[test]
fn test_total_average_energy_diatomic() {
let e = StatMech::total_average_energy(5, 300.0);
let expected = 2.5 * K_B * 300.0;
assert!((e - expected).abs() < 1e-35);
}
#[test]
fn test_total_average_energy_zero_dof() {
let e = StatMech::total_average_energy(0, 300.0);
assert_eq!(e, 0.0);
}
#[test]
fn test_fermi_dirac_below_fermi_level() {
let mu = 1e-19;
let f = StatMech::fermi_dirac(1e-22, mu, 300.0);
assert!(f > 0.99);
}
#[test]
fn test_fermi_dirac_above_fermi_level() {
let mu = 1e-22;
let f = StatMech::fermi_dirac(1e-19, mu, 300.0);
assert!(f < 0.01);
}
#[test]
fn test_fermi_dirac_at_fermi_level() {
let mu = 1e-19;
let f = StatMech::fermi_dirac(mu, mu, 300.0);
assert!((f - 0.5).abs() < 1e-10);
}
#[test]
fn test_fermi_dirac_zero_temperature_step() {
let mu = 5e-20;
let f_below = StatMech::fermi_dirac(1e-20, mu, 0.0);
let f_above = StatMech::fermi_dirac(1e-19, mu, 0.0);
assert_eq!(f_below, 1.0);
assert_eq!(f_above, 0.0);
}
#[test]
fn test_fermi_dirac_range() {
for e_scale in [1e-21, 1e-20, 1e-19] {
let f = StatMech::fermi_dirac(e_scale, 1e-20, 300.0);
assert!((0.0..=1.0).contains(&f));
}
}
#[test]
fn test_bose_einstein_high_temperature() {
let mu = 0.0;
let e = K_B * 1.0; let n = StatMech::bose_einstein(e, mu, 1e6);
assert!(n.is_some());
assert!(n.expect("should succeed") > 1.0);
}
#[test]
fn test_bose_einstein_mu_equals_e_returns_none() {
let e = 1e-19;
let result = StatMech::bose_einstein(e, e, 300.0);
assert!(result.is_none());
}
#[test]
fn test_bose_einstein_zero_temperature_returns_none() {
let result = StatMech::bose_einstein(1e-19, 0.0, 0.0);
assert!(result.is_none());
}
#[test]
fn test_bose_einstein_mu_above_e_returns_none() {
let result = StatMech::bose_einstein(1e-20, 1e-19, 300.0);
assert!(result.is_none());
}
#[test]
fn test_bose_einstein_occupation_positive() {
let e = 1e-19;
let mu = -1e-20; let n = StatMech::bose_einstein(e, mu, 300.0);
assert!(n.is_some());
assert!(n.expect("should succeed") > 0.0);
}
#[test]
fn test_mean_free_path_nitrogen_stp() {
let d = 370e-12_f64;
let n = 2.69e25_f64;
let lambda = StatMech::mean_free_path(d, n).expect("should succeed");
assert!(lambda > 50e-9 && lambda < 100e-9);
}
#[test]
fn test_mean_free_path_from_pressure() {
let lambda = StatMech::mean_free_path_from_pressure(370e-12, 101_325.0, 273.15)
.expect("should succeed");
assert!(lambda > 30e-9 && lambda < 120e-9);
}
#[test]
fn test_mean_free_path_zero_diameter_none() {
assert!(StatMech::mean_free_path(0.0, 1e25).is_none());
}
#[test]
fn test_mean_free_path_zero_density_none() {
assert!(StatMech::mean_free_path(370e-12, 0.0).is_none());
}
#[test]
fn test_mean_free_path_negative_inputs_none() {
assert!(StatMech::mean_free_path(-1e-10, 1e25).is_none());
}
#[test]
fn test_mean_free_path_increases_with_lower_density() {
let d = 370e-12_f64;
let lambda_high = StatMech::mean_free_path(d, 1e25).expect("should succeed");
let lambda_low = StatMech::mean_free_path(d, 1e20).expect("should succeed");
assert!(lambda_low > lambda_high);
}
#[test]
fn test_mb_most_probable_speed_n2_300k() {
let dist = MaxwellBoltzmannDist::new(MASS_N2, 300.0);
let v_p = dist.most_probable_speed();
assert!((v_p - 422.0).abs() < 10.0);
}
#[test]
fn test_mb_mean_speed_n2_300k() {
let dist = MaxwellBoltzmannDist::new(MASS_N2, 300.0);
let v_mean = dist.mean_speed();
assert!((v_mean - 476.0).abs() < 15.0);
}
#[test]
fn test_mb_rms_speed_n2_300k() {
let dist = MaxwellBoltzmannDist::new(MASS_N2, 300.0);
let v_rms = dist.rms_speed();
assert!((v_rms - 517.0).abs() < 15.0);
}
#[test]
fn test_mb_speed_hierarchy() {
let dist = MaxwellBoltzmannDist::new(MASS_N2, 300.0);
assert!(dist.most_probable_speed() < dist.mean_speed());
assert!(dist.mean_speed() < dist.rms_speed());
}
#[test]
fn test_mb_pdf_zero_at_zero_speed() {
let dist = MaxwellBoltzmannDist::new(MASS_N2, 300.0);
assert!((dist.pdf(0.0)).abs() < 1e-10);
}
#[test]
fn test_mb_pdf_positive_at_typical_speed() {
let dist = MaxwellBoltzmannDist::new(MASS_N2, 300.0);
assert!(dist.pdf(400.0) > 0.0);
}
#[test]
fn test_mb_pdf_negative_speed_zero() {
let dist = MaxwellBoltzmannDist::new(MASS_N2, 300.0);
assert_eq!(dist.pdf(-1.0), 0.0);
}
#[test]
fn test_mb_cdf_zero_at_zero() {
let dist = MaxwellBoltzmannDist::new(MASS_N2, 300.0);
assert!((dist.cdf(0.0)).abs() < 1e-10);
}
#[test]
fn test_mb_cdf_approaches_one_at_high_speed() {
let dist = MaxwellBoltzmannDist::new(MASS_N2, 300.0);
let cdf = dist.cdf(5000.0); assert!(cdf > 0.99);
}
#[test]
fn test_mb_cdf_monotone() {
let dist = MaxwellBoltzmannDist::new(MASS_N2, 300.0);
let v1 = 200.0;
let v2 = 500.0;
assert!(dist.cdf(v2) > dist.cdf(v1));
}
#[test]
fn test_mb_thermal_speed() {
let dist = MaxwellBoltzmannDist::new(MASS_N2, 300.0);
let a = dist.thermal_speed();
let expected = (K_B * 300.0 / MASS_N2).sqrt();
assert!((a - expected).abs() < 1e-3);
}
#[test]
fn test_erf_at_zero() {
assert!((erf(0.0)).abs() < 1e-7);
}
#[test]
fn test_erf_at_large_x_approaches_one() {
assert!((erf(5.0) - 1.0).abs() < 1e-5);
}
#[test]
fn test_erf_symmetry() {
let x = 1.2;
assert!((erf(-x) + erf(x)).abs() < 1e-7);
}
#[test]
fn test_boltzmann_constant_value() {
assert!((K_B - 1.380_649e-23).abs() < 1e-30);
}
#[test]
fn test_avogadro_constant_value() {
assert!((StatMech::N_A - 6.022_140_76e23).abs() < 1e16);
}
}