use std::f64::consts::PI;
use crate::constants::physical::{
BOLTZMANN, GAS_CONSTANT, PLANCK, REDUCED_PLANCK, SPEED_OF_LIGHT, STEFAN_BOLTZMANN,
};
use super::error::{PhysicsError, PhysicsResult};
pub fn ideal_gas_pressure(n: f64, t: f64, v: f64) -> PhysicsResult<f64> {
if n <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "n",
reason: format!("moles must be positive, got {n}"),
});
}
if t <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "t",
reason: format!("temperature must be positive (K), got {t}"),
});
}
if v <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "v",
reason: format!("volume must be positive, got {v}"),
});
}
Ok(n * GAS_CONSTANT * t / v)
}
pub fn ideal_gas_internal_energy(n: f64, t: f64) -> PhysicsResult<f64> {
if n <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "n",
reason: format!("moles must be positive, got {n}"),
});
}
if t <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "t",
reason: format!("temperature must be positive (K), got {t}"),
});
}
Ok(1.5 * n * GAS_CONSTANT * t)
}
pub fn boltzmann_distribution(energy: f64, temperature: f64) -> PhysicsResult<f64> {
if temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "temperature",
reason: format!("temperature must be positive (K), got {temperature}"),
});
}
Ok((-energy / (BOLTZMANN * temperature)).exp())
}
pub fn maxwell_speed_distribution(speed: f64, mass: f64, temperature: f64) -> PhysicsResult<f64> {
if speed < 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "speed",
reason: format!("speed must be non-negative, got {speed}"),
});
}
if mass <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "mass",
reason: format!("mass must be positive, got {mass}"),
});
}
if temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "temperature",
reason: format!("temperature must be positive (K), got {temperature}"),
});
}
let k_t = BOLTZMANN * temperature;
let a = mass / (2.0 * k_t);
let norm = 4.0 * PI * (a / PI).powf(1.5);
Ok(norm * speed * speed * (-a * speed * speed).exp())
}
pub fn maxwell_most_probable_speed(mass: f64, temperature: f64) -> PhysicsResult<f64> {
if mass <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "mass",
reason: format!("mass must be positive, got {mass}"),
});
}
if temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "temperature",
reason: format!("temperature must be positive (K), got {temperature}"),
});
}
Ok((2.0 * BOLTZMANN * temperature / mass).sqrt())
}
pub fn maxwell_mean_speed(mass: f64, temperature: f64) -> PhysicsResult<f64> {
if mass <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "mass",
reason: format!("mass must be positive, got {mass}"),
});
}
if temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "temperature",
reason: format!("temperature must be positive (K), got {temperature}"),
});
}
Ok((8.0 * BOLTZMANN * temperature / (PI * mass)).sqrt())
}
pub fn heat_capacity_debye(temperature: f64, debye_temperature: f64) -> PhysicsResult<f64> {
if temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "temperature",
reason: format!("temperature must be positive (K), got {temperature}"),
});
}
if debye_temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "debye_temperature",
reason: format!("Debye temperature must be positive (K), got {debye_temperature}"),
});
}
let x_max = debye_temperature / temperature;
if x_max < 1e-6 {
return Ok(3.0 * GAS_CONSTANT);
}
let n = 400_usize;
let h = x_max / n as f64;
let integrand = |x: f64| -> f64 {
if x < 1e-8 {
return x * x; }
let ex = x.exp();
let denom = ex - 1.0;
if denom < f64::MIN_POSITIVE {
return 0.0;
}
x.powi(4) * ex / (denom * denom)
};
let mut sum = integrand(0.0) + integrand(x_max);
for i in 1..n {
let x = i as f64 * h;
let coeff = if i % 2 == 0 { 2.0 } else { 4.0 };
sum += coeff * integrand(x);
}
let integral = sum * h / 3.0;
let ratio = temperature / debye_temperature;
Ok(9.0 * GAS_CONSTANT * ratio.powi(3) * integral)
}
pub fn black_body_spectral_radiance(wavelength: f64, temperature: f64) -> PhysicsResult<f64> {
if wavelength <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "wavelength",
reason: format!("wavelength must be positive, got {wavelength}"),
});
}
if temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "temperature",
reason: format!("temperature must be positive (K), got {temperature}"),
});
}
let hc_over_lk_t = PLANCK * SPEED_OF_LIGHT / (wavelength * BOLTZMANN * temperature);
let exp_term = hc_over_lk_t.exp() - 1.0;
if exp_term == 0.0 {
return Ok(0.0);
}
let numerator = 2.0 * PLANCK * SPEED_OF_LIGHT * SPEED_OF_LIGHT;
Ok(numerator / (wavelength.powi(5) * exp_term))
}
pub fn wien_wavelength(temperature: f64) -> PhysicsResult<f64> {
if temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "temperature",
reason: format!("temperature must be positive (K), got {temperature}"),
});
}
use crate::constants::physical::WIEN;
Ok(WIEN / temperature)
}
pub fn stefan_boltzmann_radiance(temperature: f64) -> PhysicsResult<f64> {
if temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "temperature",
reason: format!("temperature must be positive (K), got {temperature}"),
});
}
Ok(STEFAN_BOLTZMANN * temperature.powi(4))
}
pub fn thermal_de_broglie_wavelength(mass: f64, temperature: f64) -> PhysicsResult<f64> {
if mass <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "mass",
reason: format!("mass must be positive, got {mass}"),
});
}
if temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "temperature",
reason: format!("temperature must be positive (K), got {temperature}"),
});
}
Ok(PLANCK / (2.0 * PI * mass * BOLTZMANN * temperature).sqrt())
}
pub fn bose_einstein_distribution(
energy: f64,
chemical_potential: f64,
temperature: f64,
) -> PhysicsResult<f64> {
if temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "temperature",
reason: format!("temperature must be positive (K), got {temperature}"),
});
}
let x = (energy - chemical_potential) / (BOLTZMANN * temperature);
let denom = x.exp() - 1.0;
if denom <= 0.0 {
return Err(PhysicsError::DomainError(format!(
"Bose-Einstein distribution diverges for E - μ = {:.4e} J, T = {temperature} K; \
ensure E > μ",
energy - chemical_potential
)));
}
Ok(1.0 / denom)
}
pub fn fermi_dirac_distribution(
energy: f64,
chemical_potential: f64,
temperature: f64,
) -> PhysicsResult<f64> {
if temperature <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "temperature",
reason: format!("temperature must be positive (K), got {temperature}"),
});
}
let x = (energy - chemical_potential) / (BOLTZMANN * temperature);
Ok(1.0 / (x.exp() + 1.0))
}
pub fn carnot_efficiency(t_hot: f64, t_cold: f64) -> PhysicsResult<f64> {
if t_hot <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "t_hot",
reason: format!("hot reservoir temperature must be positive (K), got {t_hot}"),
});
}
if t_cold <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "t_cold",
reason: format!("cold reservoir temperature must be positive (K), got {t_cold}"),
});
}
if t_cold >= t_hot {
return Err(PhysicsError::InvalidParameter {
param: "t_cold",
reason: format!(
"cold reservoir temperature ({t_cold} K) must be less than hot ({t_hot} K)"
),
});
}
Ok(1.0 - t_cold / t_hot)
}
pub fn zero_point_energy(angular_frequency: f64) -> PhysicsResult<f64> {
if angular_frequency <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "angular_frequency",
reason: format!("angular frequency must be positive, got {angular_frequency}"),
});
}
Ok(0.5 * REDUCED_PLANCK * angular_frequency)
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-9;
#[test]
fn test_ideal_gas_pressure_stp() {
let p = ideal_gas_pressure(1.0, 273.15, 0.022_413_969_54).expect("should succeed");
assert!((p - 101_325.0).abs() < 1.0, "P = {p:.2} Pa");
}
#[test]
fn test_ideal_gas_pressure_invalid() {
assert!(ideal_gas_pressure(-1.0, 300.0, 1.0).is_err());
assert!(ideal_gas_pressure(1.0, 0.0, 1.0).is_err());
assert!(ideal_gas_pressure(1.0, 300.0, 0.0).is_err());
}
#[test]
fn test_ideal_gas_internal_energy() {
let u = ideal_gas_internal_energy(1.0, 300.0).expect("should succeed");
let expected = 1.5 * GAS_CONSTANT * 300.0;
assert!((u - expected).abs() < TOL);
}
#[test]
fn test_boltzmann_distribution_zero_energy() {
let bf = boltzmann_distribution(0.0, 300.0).expect("should succeed");
assert!((bf - 1.0).abs() < TOL);
}
#[test]
fn test_boltzmann_distribution_invalid_temperature() {
assert!(boltzmann_distribution(0.0, 0.0).is_err());
assert!(boltzmann_distribution(0.0, -10.0).is_err());
}
#[test]
fn test_maxwell_distribution_normalisation() {
let mass = crate::constants::physical::PROTON_MASS;
let temp = 1000.0;
let v_p = maxwell_most_probable_speed(mass, temp).expect("should succeed");
let v_max = 6.0 * v_p;
let n = 10_000_usize;
let dv = v_max / n as f64;
let mut sum = 0.0_f64;
for i in 0..=n {
let v = i as f64 * dv;
let f = maxwell_speed_distribution(v, mass, temp).expect("should succeed");
let coeff = if i == 0 || i == n {
1.0
} else if i % 2 == 0 {
2.0
} else {
4.0
};
sum += coeff * f;
}
let integral = sum * dv / 3.0;
assert!(
(integral - 1.0).abs() < 1e-4,
"Maxwell integral = {integral:.6}"
);
}
#[test]
fn test_maxwell_speed_invalid() {
assert!(maxwell_speed_distribution(-1.0, 1e-27, 300.0).is_err());
assert!(maxwell_speed_distribution(100.0, 0.0, 300.0).is_err());
assert!(maxwell_speed_distribution(100.0, 1e-27, 0.0).is_err());
}
#[test]
fn test_debye_high_temperature_limit() {
let cv = heat_capacity_debye(10_000.0, 100.0).expect("should succeed");
assert!(
(cv - 3.0 * GAS_CONSTANT).abs() < 0.01,
"Cv = {cv:.4} J/(mol·K)"
);
}
#[test]
fn test_debye_low_temperature() {
let cv = heat_capacity_debye(10.0, 1000.0).expect("should succeed");
assert!(cv < 3.0 * GAS_CONSTANT, "Cv = {cv:.4} should be < 3R");
assert!(cv > 0.0, "Cv must be positive");
}
#[test]
fn test_debye_invalid() {
assert!(heat_capacity_debye(0.0, 300.0).is_err());
assert!(heat_capacity_debye(300.0, 0.0).is_err());
}
#[test]
fn test_wien_wavelength_sun() {
let lambda = wien_wavelength(5778.0).expect("should succeed");
assert!(
(lambda - 5.015e-7).abs() < 2e-9,
"Wien peak = {lambda:.4e} m"
);
}
#[test]
fn test_wien_wavelength_invalid() {
assert!(wien_wavelength(0.0).is_err());
assert!(wien_wavelength(-100.0).is_err());
}
#[test]
fn test_stefan_boltzmann_sun_luminosity() {
use crate::constants::physical::SOLAR_RADIUS;
let flux = stefan_boltzmann_radiance(5778.0).expect("should succeed");
let luminosity = 4.0 * PI * SOLAR_RADIUS.powi(2) * flux;
let solar_lum = crate::constants::physical::SOLAR_LUMINOSITY;
assert!(
(luminosity - solar_lum).abs() / solar_lum < 0.05,
"L = {luminosity:.4e} W"
);
}
#[test]
fn test_planck_spectral_radiance() {
let b = black_body_spectral_radiance(5e-7, 5778.0).expect("should succeed");
assert!(b > 0.0, "Spectral radiance must be positive");
assert!(b.is_finite(), "Spectral radiance must be finite");
}
#[test]
fn test_planck_invalid() {
assert!(black_body_spectral_radiance(0.0, 300.0).is_err());
assert!(black_body_spectral_radiance(500e-9, 0.0).is_err());
}
#[test]
fn test_fermi_dirac_at_fermi_level() {
let f = fermi_dirac_distribution(1e-19, 1e-19, 1000.0).expect("should succeed");
assert!((f - 0.5).abs() < TOL);
}
#[test]
fn test_carnot_efficiency() {
let eta = carnot_efficiency(600.0, 300.0).expect("should succeed");
assert!((eta - 0.5).abs() < TOL);
}
#[test]
fn test_carnot_efficiency_invalid() {
assert!(carnot_efficiency(300.0, 300.0).is_err()); assert!(carnot_efficiency(200.0, 300.0).is_err()); assert!(carnot_efficiency(0.0, 300.0).is_err());
}
#[test]
fn test_zero_point_energy() {
let omega = 1e12_f64;
let e0 = zero_point_energy(omega).expect("should succeed");
let expected = 0.5 * REDUCED_PLANCK * omega;
assert!((e0 - expected).abs() < 1e-55);
}
}