use crate::element::GAS_CONSTANT;
use crate::error::{KimiyaError, Result};
#[inline]
pub fn arrhenius_rate(
pre_exponential: f64,
activation_energy_j: f64,
temperature_k: f64,
) -> Result<f64> {
if temperature_k <= 0.0 {
return Err(KimiyaError::InvalidTemperature(
"temperature must be positive".into(),
));
}
Ok(pre_exponential * (-activation_energy_j / (GAS_CONSTANT * temperature_k)).exp())
}
#[inline]
pub fn half_life_first_order(rate_constant: f64) -> Result<f64> {
if rate_constant <= 0.0 {
return Err(KimiyaError::InvalidInput(
"rate constant must be positive".into(),
));
}
Ok(std::f64::consts::LN_2 / rate_constant)
}
#[must_use]
#[inline]
pub fn first_order_concentration(initial: f64, rate_constant: f64, time: f64) -> f64 {
initial * (-rate_constant * time).exp()
}
#[inline]
pub fn second_order_concentration(initial: f64, rate_constant: f64, time: f64) -> Result<f64> {
if initial <= 0.0 {
return Err(KimiyaError::InvalidConcentration(
"initial concentration must be positive".into(),
));
}
Ok(1.0 / (1.0 / initial + rate_constant * time))
}
#[must_use]
#[inline]
pub fn zero_order_concentration(initial: f64, rate_constant: f64, time: f64) -> f64 {
(initial - rate_constant * time).max(0.0)
}
#[inline]
pub fn zero_order_half_life(initial: f64, rate_constant: f64) -> Result<f64> {
if rate_constant <= 0.0 {
return Err(KimiyaError::InvalidInput(
"rate constant must be positive".into(),
));
}
Ok(initial / (2.0 * rate_constant))
}
#[inline]
pub fn nth_order_half_life(initial: f64, rate_constant: f64, order: f64) -> Result<f64> {
if order <= 1.0 {
return Err(KimiyaError::InvalidInput(
"order must be > 1 (use half_life_first_order for n=1)".into(),
));
}
if rate_constant <= 0.0 {
return Err(KimiyaError::InvalidInput(
"rate constant must be positive".into(),
));
}
if initial <= 0.0 {
return Err(KimiyaError::InvalidConcentration(
"initial concentration must be positive".into(),
));
}
let n_minus_1 = order - 1.0;
Ok((2.0_f64.powf(n_minus_1) - 1.0) / (n_minus_1 * rate_constant * initial.powf(n_minus_1)))
}
#[inline]
pub fn steady_state_intermediate(k1: f64, concentration_a: f64, k2: f64) -> Result<f64> {
if k2 <= 0.0 {
return Err(KimiyaError::InvalidInput("k2 must be positive".into()));
}
Ok(k1 * concentration_a / k2)
}
#[must_use]
#[inline]
pub fn steady_state_rate(k_slow: f64, concentration: f64) -> f64 {
k_slow * concentration
}
#[inline]
pub fn pre_equilibrium_rate(
k_forward: f64,
k_reverse: f64,
k_slow: f64,
concentration_a: f64,
) -> Result<f64> {
if k_reverse <= 0.0 {
return Err(KimiyaError::InvalidInput(
"reverse rate constant must be positive".into(),
));
}
Ok(k_slow * (k_forward / k_reverse) * concentration_a)
}
#[inline]
pub fn michaelis_menten(v_max: f64, km: f64, substrate: f64) -> Result<f64> {
if v_max <= 0.0 {
return Err(KimiyaError::InvalidInput("Vmax must be positive".into()));
}
if km <= 0.0 {
return Err(KimiyaError::InvalidInput("Km must be positive".into()));
}
if substrate < 0.0 {
return Err(KimiyaError::InvalidConcentration(
"substrate concentration must be non-negative".into(),
));
}
Ok(v_max * substrate / (km + substrate))
}
#[inline]
pub fn lineweaver_burk(v_max: f64, km: f64) -> Result<(f64, f64)> {
if v_max <= 0.0 {
return Err(KimiyaError::InvalidInput("Vmax must be positive".into()));
}
if km <= 0.0 {
return Err(KimiyaError::InvalidInput("Km must be positive".into()));
}
Ok((km / v_max, 1.0 / v_max))
}
#[inline]
pub fn collision_theory_rate(
collision_frequency: f64,
steric_factor: f64,
activation_energy_j: f64,
temperature_k: f64,
) -> Result<f64> {
if temperature_k <= 0.0 {
return Err(KimiyaError::InvalidTemperature(
"temperature must be positive".into(),
));
}
if steric_factor <= 0.0 || steric_factor > 1.0 {
return Err(KimiyaError::InvalidInput(
"steric factor must be in (0, 1]".into(),
));
}
Ok(collision_frequency
* steric_factor
* (-activation_energy_j / (GAS_CONSTANT * temperature_k)).exp())
}
pub const BOLTZMANN: f64 = 1.380649e-23;
#[inline]
pub fn eyring_rate(delta_g_activation_j: f64, temperature_k: f64) -> Result<f64> {
if temperature_k <= 0.0 {
return Err(KimiyaError::InvalidTemperature(
"temperature must be positive".into(),
));
}
let prefactor = BOLTZMANN * temperature_k / crate::spectroscopy::PLANCK;
Ok(prefactor * (-delta_g_activation_j / (GAS_CONSTANT * temperature_k)).exp())
}
#[inline]
pub fn eyring_rate_from_activation(
delta_h_activation_j: f64,
delta_s_activation_j_per_k: f64,
temperature_k: f64,
) -> Result<f64> {
if temperature_k <= 0.0 {
return Err(KimiyaError::InvalidTemperature(
"temperature must be positive".into(),
));
}
let prefactor = BOLTZMANN * temperature_k / crate::spectroscopy::PLANCK;
Ok(prefactor
* (delta_s_activation_j_per_k / GAS_CONSTANT).exp()
* (-delta_h_activation_j / (GAS_CONSTANT * temperature_k)).exp())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn arrhenius_basic() {
let k = arrhenius_rate(1e13, 100_000.0, 300.0).unwrap();
assert!(
k > 0.0 && k < 1e13,
"rate should be positive and less than A, got {k}"
);
}
#[test]
fn arrhenius_higher_temp_faster() {
let k_low = arrhenius_rate(1e13, 50_000.0, 300.0).unwrap();
let k_high = arrhenius_rate(1e13, 50_000.0, 400.0).unwrap();
assert!(k_high > k_low, "higher temperature should give faster rate");
}
#[test]
fn arrhenius_higher_ea_slower() {
let k_low_ea = arrhenius_rate(1e13, 30_000.0, 300.0).unwrap();
let k_high_ea = arrhenius_rate(1e13, 100_000.0, 300.0).unwrap();
assert!(
k_low_ea > k_high_ea,
"higher activation energy should give slower rate"
);
}
#[test]
fn arrhenius_zero_temp_is_error() {
assert!(arrhenius_rate(1e13, 50_000.0, 0.0).is_err());
}
#[test]
fn half_life_basic() {
let t = half_life_first_order(0.1).unwrap();
assert!(
(t - 6.931).abs() < 0.01,
"t½ for k=0.1 should be ~6.93s, got {t}"
);
}
#[test]
fn half_life_zero_rate_is_error() {
assert!(half_life_first_order(0.0).is_err());
}
#[test]
fn first_order_at_half_life() {
let k = 0.1;
let t_half = half_life_first_order(k).unwrap();
let remaining = first_order_concentration(1.0, k, t_half);
assert!(
(remaining - 0.5).abs() < 0.001,
"should be half at t½, got {remaining}"
);
}
#[test]
fn second_order_decreases() {
let c0 = 1.0;
let c1 = second_order_concentration(c0, 0.1, 10.0).unwrap();
assert!(c1 < c0, "concentration should decrease");
assert!(c1 > 0.0, "concentration should stay positive");
}
#[test]
fn second_order_zero_initial_is_error() {
assert!(second_order_concentration(0.0, 0.1, 10.0).is_err());
}
#[test]
fn arrhenius_negative_temp_is_error() {
assert!(arrhenius_rate(1e13, 50_000.0, -100.0).is_err());
}
#[test]
fn half_life_negative_rate_is_error() {
assert!(half_life_first_order(-0.1).is_err());
}
#[test]
fn first_order_at_time_zero() {
let remaining = first_order_concentration(1.0, 0.1, 0.0);
assert!((remaining - 1.0).abs() < f64::EPSILON);
}
#[test]
fn arrhenius_zero_ea_equals_pre_exponential() {
let k = arrhenius_rate(1e13, 0.0, 300.0).unwrap();
assert!((k - 1e13).abs() < 1.0, "zero Ea should give k=A, got {k}");
}
#[test]
fn michaelis_menten_at_km() {
let v = michaelis_menten(100.0, 5.0, 5.0).unwrap();
assert!((v - 50.0).abs() < f64::EPSILON);
}
#[test]
fn michaelis_menten_saturation() {
let v = michaelis_menten(100.0, 5.0, 1e6).unwrap();
assert!((v - 100.0).abs() < 0.01);
}
#[test]
fn michaelis_menten_zero_substrate() {
let v = michaelis_menten(100.0, 5.0, 0.0).unwrap();
assert!(v.abs() < f64::EPSILON);
}
#[test]
fn michaelis_menten_zero_vmax_is_error() {
assert!(michaelis_menten(0.0, 5.0, 1.0).is_err());
}
#[test]
fn michaelis_menten_zero_km_is_error() {
assert!(michaelis_menten(100.0, 0.0, 1.0).is_err());
}
#[test]
fn lineweaver_burk_basic() {
let (slope, intercept) = lineweaver_burk(100.0, 5.0).unwrap();
assert!((slope - 0.05).abs() < 1e-10); assert!((intercept - 0.01).abs() < 1e-10); }
#[test]
fn collision_theory_basic() {
let k = collision_theory_rate(1e10, 0.1, 50_000.0, 300.0).unwrap();
assert!(k > 0.0);
assert!(k < 1e10, "rate should be less than Z×p");
}
#[test]
fn collision_theory_steric_reduces_rate() {
let k_full = collision_theory_rate(1e10, 1.0, 50_000.0, 300.0).unwrap();
let k_steric = collision_theory_rate(1e10, 0.01, 50_000.0, 300.0).unwrap();
assert!(k_steric < k_full);
}
#[test]
fn collision_theory_zero_steric_is_error() {
assert!(collision_theory_rate(1e10, 0.0, 50_000.0, 300.0).is_err());
}
#[test]
fn collision_theory_steric_over_one_is_error() {
assert!(collision_theory_rate(1e10, 1.5, 50_000.0, 300.0).is_err());
}
#[test]
fn eyring_basic() {
let k = eyring_rate(80_000.0, 298.0).unwrap();
assert!(k > 0.0 && k < 1e15);
}
#[test]
fn eyring_higher_temp_faster() {
let k_low = eyring_rate(80_000.0, 298.0).unwrap();
let k_high = eyring_rate(80_000.0, 400.0).unwrap();
assert!(k_high > k_low);
}
#[test]
fn eyring_zero_temp_is_error() {
assert!(eyring_rate(80_000.0, 0.0).is_err());
}
#[test]
fn eyring_decomposed_matches_combined() {
let dh = 80_000.0;
let ds = -50.0;
let t = 298.0;
let dg = dh - t * ds;
let k_combined = eyring_rate(dg, t).unwrap();
let k_decomposed = eyring_rate_from_activation(dh, ds, t).unwrap();
assert!(
(k_combined - k_decomposed).abs() / k_combined < 1e-6,
"combined ({k_combined}) should match decomposed ({k_decomposed})"
);
}
#[test]
fn zero_order_linear_decrease() {
let c = zero_order_concentration(1.0, 0.1, 5.0);
assert!((c - 0.5).abs() < f64::EPSILON);
}
#[test]
fn zero_order_floors_at_zero() {
let c = zero_order_concentration(1.0, 0.1, 20.0);
assert!((c).abs() < f64::EPSILON, "should floor at 0, got {c}");
}
#[test]
fn zero_order_half_life_basic() {
let t = zero_order_half_life(1.0, 0.1).unwrap();
assert!((t - 5.0).abs() < f64::EPSILON);
}
#[test]
fn nth_order_second_order_matches() {
let t_nth = nth_order_half_life(1.0, 0.1, 2.0).unwrap();
let t_direct = 1.0 / (0.1 * 1.0);
assert!(
(t_nth - t_direct).abs() < 0.001,
"n=2 half-life should be {t_direct}, got {t_nth}"
);
}
#[test]
fn nth_order_increases_with_order() {
let t2 = nth_order_half_life(1.0, 0.1, 2.0).unwrap();
let t3 = nth_order_half_life(1.0, 0.1, 3.0).unwrap();
assert!(
t3 > t2,
"higher order should have longer half-life at same [A]₀"
);
}
#[test]
fn nth_order_below_1_is_error() {
assert!(nth_order_half_life(1.0, 0.1, 1.0).is_err());
assert!(nth_order_half_life(1.0, 0.1, 0.5).is_err());
}
#[test]
fn steady_state_basic() {
let b_ss = steady_state_intermediate(0.1, 1.0, 1.0).unwrap();
assert!((b_ss - 0.1).abs() < f64::EPSILON);
}
#[test]
fn steady_state_rate_basic() {
let rate = steady_state_rate(0.05, 2.0);
assert!((rate - 0.1).abs() < f64::EPSILON);
}
#[test]
fn pre_equilibrium_basic() {
let rate = pre_equilibrium_rate(10.0, 2.0, 0.1, 1.0).unwrap();
assert!((rate - 0.5).abs() < 1e-10);
}
#[test]
fn pre_equilibrium_zero_kreverse_is_error() {
assert!(pre_equilibrium_rate(1.0, 0.0, 0.1, 1.0).is_err());
}
}