#![allow(dead_code)]
pub const FARADAY: f64 = 96_485.332_12;
pub const GAS_CONSTANT: f64 = 8.314_462_618;
#[inline]
fn clamp(x: f64, lo: f64, hi: f64) -> f64 {
x.max(lo).min(hi)
}
#[inline]
fn safe_ln(x: f64) -> f64 {
if x <= 0.0 { f64::NEG_INFINITY } else { x.ln() }
}
#[derive(Debug, Clone)]
pub struct ButlerVolmer {
pub i0: f64,
pub alpha_a: f64,
pub alpha_c: f64,
pub temperature: f64,
}
impl ButlerVolmer {
pub fn new(i0: f64, alpha_a: f64, alpha_c: f64, temperature: f64) -> Self {
Self {
i0,
alpha_a,
alpha_c,
temperature,
}
}
#[inline]
pub fn f_over_rt(&self) -> f64 {
FARADAY / (GAS_CONSTANT * self.temperature.max(1e-6))
}
pub fn current_density(&self, eta: f64) -> f64 {
let q = self.f_over_rt();
self.i0 * ((self.alpha_a * q * eta).exp() - (-self.alpha_c * q * eta).exp())
}
pub fn charge_transfer_resistance(&self) -> f64 {
GAS_CONSTANT * self.temperature
/ (self.i0.max(1e-30) * (self.alpha_a + self.alpha_c) * FARADAY)
}
pub fn tafel_slope_anodic(&self) -> f64 {
std::f64::consts::LN_10 / (self.alpha_a * self.f_over_rt())
}
pub fn tafel_slope_cathodic(&self) -> f64 {
std::f64::consts::LN_10 / (self.alpha_c * self.f_over_rt())
}
pub fn tafel_overpotential(&self, i_target: f64) -> f64 {
if i_target > 0.0 {
self.tafel_slope_anodic() * safe_ln(i_target / self.i0.max(1e-30))
/ std::f64::consts::LN_10
} else if i_target < 0.0 {
-self.tafel_slope_cathodic() * safe_ln((-i_target) / self.i0.max(1e-30))
/ std::f64::consts::LN_10
} else {
0.0
}
}
}
#[derive(Debug, Clone)]
pub struct EvansDiagram {
pub anodic: ButlerVolmer,
pub e_eq_anodic: f64,
pub cathodic: ButlerVolmer,
pub e_eq_cathodic: f64,
}
impl EvansDiagram {
pub fn new(
anodic: ButlerVolmer,
e_eq_anodic: f64,
cathodic: ButlerVolmer,
e_eq_cathodic: f64,
) -> Self {
Self {
anodic,
e_eq_anodic,
cathodic,
e_eq_cathodic,
}
}
pub fn net_current(&self, e: f64) -> f64 {
let eta_a = e - self.e_eq_anodic;
let eta_c = e - self.e_eq_cathodic;
self.anodic.current_density(eta_a) + self.cathodic.current_density(eta_c)
}
pub fn corrosion_potential(&self, e_lo: f64, e_hi: f64, tol: f64) -> Option<f64> {
let fa = self.net_current(e_lo);
let fb = self.net_current(e_hi);
if fa * fb > 0.0 {
return None;
}
let mut lo = e_lo;
let mut hi = e_hi;
for _ in 0..100 {
let mid = (lo + hi) * 0.5;
if hi - lo < tol {
return Some(mid);
}
let fm = self.net_current(mid);
if fa * fm <= 0.0 {
hi = mid;
} else {
lo = mid;
}
}
Some((lo + hi) * 0.5)
}
pub fn corrosion_current(&self, e_corr: f64) -> f64 {
let eta_a = e_corr - self.e_eq_anodic;
self.anodic.current_density(eta_a).abs()
}
}
pub fn corrosion_mass_rate(i_corr: f64, molar_mass: f64, n_electrons: f64) -> f64 {
i_corr * molar_mass / (n_electrons * FARADAY)
}
pub fn corrosion_rate_mm_per_year(
i_corr: f64,
molar_mass: f64,
n_electrons: f64,
density: f64,
) -> f64 {
let mass_rate = corrosion_mass_rate(i_corr, molar_mass, n_electrons);
let thickness_rate_m_per_s = mass_rate / (density * 1e6); thickness_rate_m_per_s * 1e3 * 3.156e7 }
pub fn corrosion_rate_mpy(i_corr: f64, molar_mass: f64, n_electrons: f64, density: f64) -> f64 {
corrosion_rate_mm_per_year(i_corr, molar_mass, n_electrons, density) / 0.0254
}
#[derive(Debug, Clone)]
pub struct PassivationModel {
pub flade_potential: f64,
pub i_passivation: f64,
pub i_passive: f64,
pub e_transpassive: f64,
pub active_bv: ButlerVolmer,
}
impl PassivationModel {
pub fn new(
flade_potential: f64,
i_passivation: f64,
i_passive: f64,
e_transpassive: f64,
active_bv: ButlerVolmer,
) -> Self {
Self {
flade_potential,
i_passivation,
i_passive,
e_transpassive,
active_bv,
}
}
pub fn current_density(&self, e: f64) -> f64 {
if e < self.flade_potential {
let eta = e - self.active_bv.f_over_rt() * 0.0; self.active_bv.current_density(eta).max(0.0)
} else if e < self.e_transpassive {
self.i_passive
} else {
self.i_passive + (e - self.e_transpassive) * self.i_passivation
}
}
pub fn is_passive(&self, e: f64) -> bool {
e >= self.flade_potential && e < self.e_transpassive
}
pub fn passive_range(&self) -> f64 {
(self.e_transpassive - self.flade_potential).max(0.0)
}
}
#[derive(Debug, Clone)]
pub struct PittingModel {
pub e_pit: f64,
pub e_rp: f64,
pub critical_chloride: f64,
pub pit_growth_k: f64,
}
impl PittingModel {
pub fn new(e_pit: f64, e_rp: f64, critical_chloride: f64, pit_growth_k: f64) -> Self {
Self {
e_pit,
e_rp,
critical_chloride,
pit_growth_k,
}
}
pub fn hysteresis_width(&self) -> f64 {
(self.e_pit - self.e_rp).max(0.0)
}
pub fn is_pitting_active(&self, e: f64) -> bool {
e >= self.e_pit
}
pub fn will_repassivate(&self, e: f64) -> bool {
e < self.e_rp
}
pub fn pit_radius(&self, i_pit: f64, t: f64, molar_mass: f64, n: f64, density: f64) -> f64 {
let rho_si = density * 1e6; let numerator = 3.0 * molar_mass * i_pit * t;
let denominator = 2.0 * std::f64::consts::PI * n * FARADAY * rho_si;
if denominator <= 0.0 {
0.0
} else {
(numerator / denominator).cbrt()
}
}
pub fn induction_time(&self, chloride_conc: f64, material_constant_s: f64) -> Option<f64> {
let ratio = chloride_conc / self.critical_chloride.max(1e-30);
if ratio <= 1.0 {
None } else {
Some(material_constant_s / (ratio - 1.0))
}
}
}
#[derive(Debug, Clone)]
pub struct GalvanicPair {
pub anode: ButlerVolmer,
pub e_eq_anode: f64,
pub cathode: ButlerVolmer,
pub e_eq_cathode: f64,
pub area_ratio: f64,
}
impl GalvanicPair {
pub fn new(
anode: ButlerVolmer,
e_eq_anode: f64,
cathode: ButlerVolmer,
e_eq_cathode: f64,
area_ratio: f64,
) -> Self {
Self {
anode,
e_eq_anode,
cathode,
e_eq_cathode,
area_ratio,
}
}
pub fn driving_force(&self) -> f64 {
(self.e_eq_cathode - self.e_eq_anode).abs()
}
pub fn net_current(&self, e: f64) -> f64 {
let i_a = self.anode.current_density(e - self.e_eq_anode);
let i_c = self.cathode.current_density(e - self.e_eq_cathode) * self.area_ratio;
i_a + i_c
}
pub fn galvanic_potential(&self, e_lo: f64, e_hi: f64, tol: f64) -> Option<f64> {
let fa = self.net_current(e_lo);
let fb = self.net_current(e_hi);
if fa * fb > 0.0 {
return None;
}
let mut lo = e_lo;
let mut hi = e_hi;
for _ in 0..100 {
let mid = (lo + hi) * 0.5;
if hi - lo < tol {
return Some(mid);
}
let fm = self.net_current(mid);
if fa * fm <= 0.0 {
hi = mid;
} else {
lo = mid;
}
}
Some((lo + hi) * 0.5)
}
}
#[derive(Debug, Clone)]
pub struct SccrModel {
pub k_iscc: f64,
pub k_ic: f64,
pub v_plateau: f64,
pub stage1_exponent: f64,
pub stage1_coeff: f64,
}
impl SccrModel {
pub fn new(
k_iscc: f64,
k_ic: f64,
v_plateau: f64,
stage1_exponent: f64,
stage1_coeff: f64,
) -> Self {
Self {
k_iscc,
k_ic,
v_plateau,
stage1_exponent,
stage1_coeff,
}
}
pub fn crack_velocity(&self, ki: f64) -> f64 {
if ki < self.k_iscc {
0.0
} else if ki >= self.k_ic {
f64::INFINITY
} else {
let dk = ki - self.k_iscc;
let v_stage1 = self.stage1_coeff * dk.powf(self.stage1_exponent);
v_stage1.min(self.v_plateau)
}
}
#[allow(clippy::too_many_arguments)]
pub fn time_to_fracture(
&self,
a0: f64,
sigma: f64,
geometry_factor_y: f64,
steps: usize,
) -> f64 {
let ac = (self.k_ic / (geometry_factor_y * sigma * std::f64::consts::PI.sqrt())).powi(2);
if a0 >= ac {
return 0.0;
}
let da = (ac - a0) / steps.max(1) as f64;
let ki = |a: f64| geometry_factor_y * sigma * (std::f64::consts::PI * a).sqrt();
let dt = |a: f64| {
let v = self.crack_velocity(ki(a));
if v <= 0.0 || v.is_infinite() {
0.0
} else {
da / v
}
};
let mut t = 0.0f64;
let mut a = a0;
for _ in 0..steps {
t += dt(a);
a += da;
}
t
}
pub fn susceptibility_index(&self) -> f64 {
(self.k_iscc / self.k_ic.max(1e-30)).min(1.0)
}
}
#[derive(Debug, Clone)]
pub struct CreviceModel {
pub gap: f64,
pub depth: f64,
pub resistivity: f64,
pub e_crevice_critical: f64,
pub i_passive: f64,
}
impl CreviceModel {
pub fn new(
gap: f64,
depth: f64,
resistivity: f64,
e_crevice_critical: f64,
i_passive: f64,
) -> Self {
Self {
gap,
depth,
resistivity,
e_crevice_critical,
i_passive,
}
}
pub fn ir_drop(&self) -> f64 {
self.i_passive * self.resistivity * self.depth.powi(2) / (2.0 * self.gap.max(1e-15))
}
pub fn critical_depth(&self, e_external: f64) -> Option<f64> {
let dv_crit = e_external - self.e_crevice_critical;
if dv_crit <= 0.0 {
return None;
}
let arg =
2.0 * self.gap * dv_crit / (self.i_passive.max(1e-30) * self.resistivity.max(1e-30));
Some(arg.sqrt())
}
pub fn geometry_factor(&self, e_external: f64) -> f64 {
let dv = (e_external - self.e_crevice_critical).max(1e-30);
self.i_passive * self.resistivity * self.depth.powi(2) / (2.0 * self.gap.max(1e-15) * dv)
}
}
#[derive(Debug, Clone)]
pub struct DealloyingModel {
pub x0: f64,
pub e_threshold: f64,
pub d_alloy: f64,
pub k_dissolution: f64,
}
impl DealloyingModel {
pub fn new(x0: f64, e_threshold: f64, d_alloy: f64, k_dissolution: f64) -> Self {
Self {
x0,
e_threshold,
d_alloy,
k_dissolution,
}
}
pub fn is_active(&self, e: f64) -> bool {
e >= self.e_threshold
}
pub fn layer_thickness(&self, t: f64) -> f64 {
(2.0 * self.d_alloy * t).sqrt()
}
pub fn residual_fraction(&self, delta: f64) -> f64 {
let decay = (self.k_dissolution * delta / self.d_alloy.max(1e-30)).min(700.0);
self.x0 * (-decay).exp()
}
pub fn dissolved_moles_per_area(&self, t: f64) -> f64 {
self.k_dissolution * t
}
}
#[derive(Debug, Clone)]
pub struct ImpressedCurrentCp {
pub surface_area: f64,
pub protective_current_density: f64,
pub efficiency: f64,
pub design_life_years: f64,
}
impl ImpressedCurrentCp {
pub fn new(
surface_area: f64,
protective_current_density: f64,
efficiency: f64,
design_life_years: f64,
) -> Self {
Self {
surface_area,
protective_current_density,
efficiency,
design_life_years,
}
}
pub fn required_current(&self) -> f64 {
self.surface_area * self.protective_current_density
}
pub fn rectifier_current(&self) -> f64 {
self.required_current() / self.efficiency.max(1e-6)
}
pub fn total_charge_ah(&self) -> f64 {
self.rectifier_current() * self.design_life_years * 8760.0
}
}
#[derive(Debug, Clone)]
pub struct SacrificialAnode {
pub electrochemical_capacity: f64,
pub current_output: f64,
pub design_life_years: f64,
pub current_efficiency: f64,
}
impl SacrificialAnode {
pub fn new(
electrochemical_capacity: f64,
current_output: f64,
design_life_years: f64,
current_efficiency: f64,
) -> Self {
Self {
electrochemical_capacity,
current_output,
design_life_years,
current_efficiency,
}
}
pub fn anode_mass(&self) -> f64 {
let ah_required = self.current_output * self.design_life_years * 8760.0;
ah_required / (self.electrochemical_capacity * self.current_efficiency.max(1e-6))
}
pub fn anode_count(&self, total_current: f64) -> u64 {
let n = (total_current / self.current_output.max(1e-30)).ceil();
n as u64
}
}
#[derive(Debug, Clone)]
pub struct CorrosionInhibitor {
pub langmuir_k: f64,
pub concentration: f64,
pub uninhibited_rate: f64,
}
impl CorrosionInhibitor {
pub fn new(langmuir_k: f64, concentration: f64, uninhibited_rate: f64) -> Self {
Self {
langmuir_k,
concentration,
uninhibited_rate,
}
}
pub fn surface_coverage(&self) -> f64 {
let kc = self.langmuir_k * self.concentration;
kc / (1.0 + kc)
}
pub fn inhibited_rate(&self) -> f64 {
self.uninhibited_rate * (1.0 - self.surface_coverage())
}
pub fn efficiency_percent(&self) -> f64 {
100.0 * self.surface_coverage()
}
pub fn concentration_for_efficiency(&self, ie_target: f64) -> Option<f64> {
let theta = clamp(ie_target, 0.0, 0.9999);
if self.langmuir_k <= 0.0 {
return None;
}
Some(theta / (self.langmuir_k * (1.0 - theta)))
}
}
#[derive(Debug, Clone)]
pub struct NernstEquation {
pub e_standard: f64,
pub n_electrons: f64,
pub temperature: f64,
}
impl NernstEquation {
pub fn new(e_standard: f64, n_electrons: f64, temperature: f64) -> Self {
Self {
e_standard,
n_electrons,
temperature,
}
}
pub fn equilibrium_potential(&self, a_ox: f64, a_red: f64) -> f64 {
let rt_nf = GAS_CONSTANT * self.temperature / (self.n_electrons * FARADAY);
self.e_standard + rt_nf * safe_ln(a_ox / a_red.max(1e-30))
}
pub fn nernst_slope_per_decade(&self) -> f64 {
GAS_CONSTANT * self.temperature * std::f64::consts::LN_10 / (self.n_electrons * FARADAY)
}
}
#[derive(Debug, Clone)]
pub struct PourbaixLine {
pub label: String,
pub a: f64,
pub b: f64,
}
impl PourbaixLine {
pub fn new(label: &str, a: f64, b: f64) -> Self {
Self {
label: label.to_string(),
a,
b,
}
}
pub fn potential(&self, ph: f64) -> f64 {
self.a + self.b * ph
}
pub fn ph_at_potential(&self, e: f64) -> Option<f64> {
if self.b.abs() < 1e-15 {
None
} else {
Some((e - self.a) / self.b)
}
}
}
#[derive(Debug, Clone)]
pub struct PourbaixDiagram {
pub lines: Vec<PourbaixLine>,
pub metal: String,
}
impl PourbaixDiagram {
pub fn new(metal: &str) -> Self {
Self {
metal: metal.to_string(),
lines: Vec::new(),
}
}
pub fn add_line(&mut self, line: PourbaixLine) {
self.lines.push(line);
}
pub fn iron() -> Self {
let mut d = Self::new("Fe");
d.add_line(PourbaixLine::new("Fe/Fe2+", -0.44, -0.0592));
d.add_line(PourbaixLine::new("Fe2+/Fe3O4", -0.059, -0.0888));
d.add_line(PourbaixLine::new("Fe3O4/Fe2O3", 0.221, -0.0592));
d.add_line(PourbaixLine::new("H2/H2O", 0.0, -0.0592));
d.add_line(PourbaixLine::new("O2/H2O", 1.229, -0.0592));
d
}
pub fn potentials_at_ph(&self, ph: f64) -> Vec<(String, f64)> {
let mut v: Vec<(String, f64)> = self
.lines
.iter()
.map(|l| (l.label.clone(), l.potential(ph)))
.collect();
v.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
v
}
}
#[derive(Debug, Clone)]
pub struct DiffusionLimitedCorrosion {
pub n_electrons: f64,
pub diffusivity: f64,
pub c_bulk: f64,
pub delta: f64,
}
impl DiffusionLimitedCorrosion {
pub fn new(n_electrons: f64, diffusivity: f64, c_bulk: f64, delta: f64) -> Self {
Self {
n_electrons,
diffusivity,
c_bulk,
delta,
}
}
pub fn limiting_current(&self) -> f64 {
self.n_electrons * FARADAY * self.diffusivity * self.c_bulk / self.delta.max(1e-15)
}
pub fn corrosion_rate_mm_per_year(&self, molar_mass: f64, n_anodic: f64, density: f64) -> f64 {
corrosion_rate_mm_per_year(self.limiting_current(), molar_mass, n_anodic, density)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn standard_bv() -> ButlerVolmer {
ButlerVolmer::new(1e-3, 0.5, 0.5, 298.15)
}
#[test]
fn test_bv_zero_overpotential() {
let bv = standard_bv();
let i = bv.current_density(0.0);
assert!(i.abs() < 1e-10, "i at η=0 should be 0, got {i}");
}
#[test]
fn test_bv_positive_overpotential_anodic() {
let bv = standard_bv();
assert!(bv.current_density(0.1) > 0.0);
}
#[test]
fn test_bv_negative_overpotential_cathodic() {
let bv = standard_bv();
assert!(bv.current_density(-0.1) < 0.0);
}
#[test]
fn test_bv_charge_transfer_resistance_positive() {
let bv = standard_bv();
assert!(bv.charge_transfer_resistance() > 0.0);
}
#[test]
fn test_bv_tafel_slopes_positive() {
let bv = standard_bv();
assert!(bv.tafel_slope_anodic() > 0.0);
assert!(bv.tafel_slope_cathodic() > 0.0);
}
#[test]
fn test_bv_tafel_slope_symmetric() {
let bv = standard_bv(); let diff = (bv.tafel_slope_anodic() - bv.tafel_slope_cathodic()).abs();
assert!(
diff < 1e-10,
"symmetric αa=αc should give equal Tafel slopes, diff={diff}"
);
}
#[test]
fn test_bv_tafel_overpotential_sign() {
let bv = standard_bv();
assert!(bv.tafel_overpotential(1e-2) > 0.0);
assert!(bv.tafel_overpotential(-1e-2) < 0.0);
assert_eq!(bv.tafel_overpotential(0.0), 0.0);
}
#[test]
fn test_bv_f_over_rt_positive() {
let bv = standard_bv();
assert!(bv.f_over_rt() > 0.0);
}
#[test]
fn test_evans_corrosion_potential_found() {
let anodic = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let cathodic = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let diagram = EvansDiagram::new(anodic, -0.5, cathodic, 0.0);
let e_corr = diagram.corrosion_potential(-0.5, 0.0, 1e-6);
assert!(e_corr.is_some());
}
#[test]
fn test_evans_net_current_sign_change() {
let anodic = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let cathodic = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let diagram = EvansDiagram::new(anodic, -0.5, cathodic, 0.0);
let ia = diagram.net_current(-0.5);
let ib = diagram.net_current(0.0);
assert!(ia < 0.0, "net at e_eq_anode should be < 0, got {ia}");
assert!(ib > 0.0, "net at e_eq_cathode should be > 0, got {ib}");
}
#[test]
fn test_evans_corrosion_current_positive() {
let anodic = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let cathodic = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let diagram = EvansDiagram::new(anodic, -0.5, cathodic, 0.0);
if let Some(e_corr) = diagram.corrosion_potential(-0.5, 0.0, 1e-6) {
assert!(diagram.corrosion_current(e_corr) >= 0.0);
}
}
#[test]
fn test_corrosion_mass_rate_positive() {
let rate = corrosion_mass_rate(1.0, 55.845, 2.0);
assert!(rate > 0.0);
}
#[test]
fn test_corrosion_rate_mm_per_year_positive() {
let rate = corrosion_rate_mm_per_year(1.0, 55.845, 2.0, 7.87);
assert!(rate > 0.0);
}
#[test]
fn test_corrosion_rate_mpy_greater_than_mm() {
let mm = corrosion_rate_mm_per_year(1.0, 55.845, 2.0, 7.87);
let mpy = corrosion_rate_mpy(1.0, 55.845, 2.0, 7.87);
assert!(mpy > mm, "mpy={mpy} should be > mm/year={mm}");
}
#[test]
fn test_passivation_is_passive() {
let bv = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let model = PassivationModel::new(-0.2, 1e-2, 1e-5, 1.5, bv);
assert!(model.is_passive(0.5));
assert!(!model.is_passive(-0.5));
assert!(!model.is_passive(2.0));
}
#[test]
fn test_passivation_passive_range() {
let bv = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let model = PassivationModel::new(-0.2, 1e-2, 1e-5, 1.5, bv);
assert!((model.passive_range() - 1.7).abs() < 1e-10);
}
#[test]
fn test_passivation_current_passive_plateau() {
let bv = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let model = PassivationModel::new(-0.2, 1e-2, 1e-5, 1.5, bv);
let i = model.current_density(0.5);
assert!(
(i - 1e-5).abs() < 1e-12,
"expected passive i_passive, got {i}"
);
}
#[test]
fn test_pitting_active() {
let model = PittingModel::new(0.3, 0.1, 0.01, 1e-8);
assert!(model.is_pitting_active(0.5));
assert!(!model.is_pitting_active(0.2));
}
#[test]
fn test_pitting_repassivation() {
let model = PittingModel::new(0.3, 0.1, 0.01, 1e-8);
assert!(model.will_repassivate(0.05));
assert!(!model.will_repassivate(0.15));
}
#[test]
fn test_pitting_pit_radius_grows_with_time() {
let model = PittingModel::new(0.3, 0.1, 0.01, 1e-8);
let r1 = model.pit_radius(100.0, 3600.0, 55.845, 2.0, 7.87);
let r2 = model.pit_radius(100.0, 7200.0, 55.845, 2.0, 7.87);
assert!(r2 > r1);
}
#[test]
fn test_pitting_induction_time_none_below_threshold() {
let model = PittingModel::new(0.3, 0.1, 0.01, 1e-8);
let t = model.induction_time(0.005, 1000.0); assert!(t.is_none());
}
#[test]
fn test_pitting_induction_time_some_above_threshold() {
let model = PittingModel::new(0.3, 0.1, 0.01, 1e-8);
let t = model.induction_time(0.02, 1000.0);
assert!(t.is_some());
assert!(t.unwrap() > 0.0);
}
#[test]
fn test_pitting_hysteresis_width() {
let model = PittingModel::new(0.5, 0.2, 0.01, 1e-8);
assert!((model.hysteresis_width() - 0.3).abs() < 1e-10);
}
#[test]
fn test_galvanic_driving_force() {
let a = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let c = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let pair = GalvanicPair::new(a, -0.44, c, 0.34, 10.0);
assert!((pair.driving_force() - 0.78).abs() < 1e-10);
}
#[test]
fn test_galvanic_potential_found() {
let a = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let c = ButlerVolmer::new(1e-4, 0.5, 0.5, 298.15);
let pair = GalvanicPair::new(a, -0.44, c, 0.34, 1.0);
let e_g = pair.galvanic_potential(-0.44, 0.34, 1e-6);
assert!(e_g.is_some(), "galvanic potential should be found");
}
#[test]
fn test_scc_no_growth_below_kiscc() {
let model = SccrModel::new(10.0, 50.0, 1e-7, 2.0, 1e-9);
assert_eq!(model.crack_velocity(5.0), 0.0);
}
#[test]
fn test_scc_infinite_at_kic() {
let model = SccrModel::new(10.0, 50.0, 1e-7, 2.0, 1e-9);
assert!(model.crack_velocity(50.0).is_infinite());
}
#[test]
fn test_scc_velocity_finite_midrange() {
let model = SccrModel::new(10.0, 50.0, 1e-7, 2.0, 1e-9);
let v = model.crack_velocity(25.0);
assert!(v.is_finite() && v >= 0.0);
}
#[test]
fn test_scc_susceptibility_index_bounded() {
let model = SccrModel::new(10.0, 50.0, 1e-7, 2.0, 1e-9);
let s = model.susceptibility_index();
assert!((0.0..=1.0).contains(&s));
}
#[test]
fn test_scc_time_to_fracture_positive() {
let model = SccrModel::new(10.0, 80.0, 1e-7, 2.0, 1e-9);
let t = model.time_to_fracture(1e-4, 100.0, 1.0, 100);
assert!(t > 0.0 && t.is_finite());
}
#[test]
fn test_crevice_ir_drop_positive() {
let model = CreviceModel::new(1e-4, 1e-2, 0.1, -0.3, 1e-5);
assert!(model.ir_drop() > 0.0);
}
#[test]
fn test_crevice_critical_depth_some() {
let model = CreviceModel::new(1e-4, 1e-2, 0.1, -0.3, 1e-5);
let d = model.critical_depth(-0.1);
assert!(d.is_some());
assert!(d.unwrap() > 0.0);
}
#[test]
fn test_crevice_critical_depth_none_below_critical() {
let model = CreviceModel::new(1e-4, 1e-2, 0.1, 0.5, 1e-5);
let d = model.critical_depth(0.3);
assert!(d.is_none());
}
#[test]
fn test_dealloying_active() {
let model = DealloyingModel::new(0.3, 0.1, 1e-15, 1e-8);
assert!(model.is_active(0.2));
assert!(!model.is_active(0.05));
}
#[test]
fn test_dealloying_layer_grows_with_time() {
let model = DealloyingModel::new(0.3, 0.1, 1e-15, 1e-8);
let d1 = model.layer_thickness(1000.0);
let d2 = model.layer_thickness(4000.0);
assert!(d2 > d1);
}
#[test]
fn test_dealloying_residual_fraction_decreases() {
let model = DealloyingModel::new(0.3, 0.1, 1e-15, 1e-8);
let x1 = model.residual_fraction(1e-6);
let x2 = model.residual_fraction(1e-5);
assert!(x2 <= x1);
}
#[test]
fn test_impressed_current_required() {
let cp = ImpressedCurrentCp::new(500.0, 0.02, 0.9, 20.0);
assert!((cp.required_current() - 10.0).abs() < 1e-10);
}
#[test]
fn test_impressed_current_rectifier_greater_than_required() {
let cp = ImpressedCurrentCp::new(500.0, 0.02, 0.9, 20.0);
assert!(cp.rectifier_current() >= cp.required_current());
}
#[test]
fn test_sacrificial_anode_mass_positive() {
let anode = SacrificialAnode::new(780.0, 0.1, 20.0, 0.85);
assert!(anode.anode_mass() > 0.0);
}
#[test]
fn test_sacrificial_anode_count() {
let anode = SacrificialAnode::new(780.0, 0.1, 20.0, 0.85);
let n = anode.anode_count(1.0);
assert!(n > 0);
}
#[test]
fn test_inhibitor_coverage_between_0_and_1() {
let inh = CorrosionInhibitor::new(100.0, 0.01, 5.0);
let theta = inh.surface_coverage();
assert!(theta > 0.0 && theta < 1.0);
}
#[test]
fn test_inhibitor_rate_less_than_uninhibited() {
let inh = CorrosionInhibitor::new(100.0, 0.01, 5.0);
assert!(inh.inhibited_rate() < inh.uninhibited_rate);
}
#[test]
fn test_inhibitor_efficiency_positive() {
let inh = CorrosionInhibitor::new(50.0, 0.05, 3.0);
assert!(inh.efficiency_percent() > 0.0 && inh.efficiency_percent() < 100.0);
}
#[test]
fn test_inhibitor_concentration_for_efficiency() {
let inh = CorrosionInhibitor::new(100.0, 0.0, 5.0); let c = inh.concentration_for_efficiency(0.9);
assert!(c.is_some());
assert!(c.unwrap() > 0.0);
}
#[test]
fn test_nernst_standard_potential_at_unit_activity() {
let nernst = NernstEquation::new(-0.44, 2.0, 298.15);
let e = nernst.equilibrium_potential(1.0, 1.0);
assert!((e - (-0.44)).abs() < 1e-10);
}
#[test]
fn test_nernst_slope_positive() {
let nernst = NernstEquation::new(0.0, 1.0, 298.15);
assert!(nernst.nernst_slope_per_decade() > 0.0);
}
#[test]
fn test_nernst_59mv_rule() {
let nernst = NernstEquation::new(0.0, 1.0, 298.15);
let slope = nernst.nernst_slope_per_decade();
assert!(
(slope - 0.05916).abs() < 2e-4,
"Expected ~59.16 mV/decade, got {}",
slope * 1000.0
);
}
#[test]
fn test_pourbaix_line_potential() {
let line = PourbaixLine::new("test", 0.0, -0.0592);
let e = line.potential(7.0);
assert!((e - (-0.0592 * 7.0)).abs() < 1e-10);
}
#[test]
fn test_pourbaix_line_ph_at_potential() {
let line = PourbaixLine::new("test", 0.0, -0.0592);
let e_target = -0.0592 * 7.0; let ph = line.ph_at_potential(e_target);
assert!(ph.is_some());
assert!(
(ph.unwrap() - 7.0).abs() < 1e-9,
"expected pH≈7 but got {}",
ph.unwrap()
);
}
#[test]
fn test_pourbaix_iron_lines_count() {
let d = PourbaixDiagram::iron();
assert!(d.lines.len() >= 5);
}
#[test]
fn test_pourbaix_potentials_at_ph_sorted() {
let d = PourbaixDiagram::iron();
let potentials = d.potentials_at_ph(7.0);
for i in 1..potentials.len() {
assert!(potentials[i].1 >= potentials[i - 1].1);
}
}
#[test]
fn test_diffusion_limiting_current_positive() {
let model = DiffusionLimitedCorrosion::new(4.0, 2e-9, 8.0, 1e-4);
assert!(model.limiting_current() > 0.0);
}
#[test]
fn test_diffusion_corrosion_rate_positive() {
let model = DiffusionLimitedCorrosion::new(4.0, 2e-9, 8.0, 1e-4);
let rate = model.corrosion_rate_mm_per_year(55.845, 2.0, 7.87);
assert!(rate > 0.0);
}
}