#[allow(unused_imports)]
use super::functions::*;
use super::functions::{FARADAY, GAS_CONSTANT};
#[derive(Debug, Clone)]
pub struct EvansDiagram {
pub i0_anodic: f64,
pub e0_anodic: f64,
pub alpha_a: f64,
pub i0_cathodic: f64,
pub e0_cathodic: f64,
pub alpha_c: f64,
pub temperature: f64,
}
impl EvansDiagram {
#[allow(clippy::too_many_arguments)]
pub fn new(
i0_anodic: f64,
e0_anodic: f64,
alpha_a: f64,
i0_cathodic: f64,
e0_cathodic: f64,
alpha_c: f64,
temperature: f64,
) -> Self {
Self {
i0_anodic,
e0_anodic,
alpha_a,
i0_cathodic,
e0_cathodic,
alpha_c,
temperature,
}
}
pub fn f_over_rt(&self) -> f64 {
FARADAY / (GAS_CONSTANT * self.temperature)
}
pub fn anodic_current(&self, e: f64) -> f64 {
let q = self.f_over_rt();
self.i0_anodic * (self.alpha_a * q * (e - self.e0_anodic)).exp()
}
pub fn cathodic_current(&self, e: f64) -> f64 {
let q = self.f_over_rt();
self.i0_cathodic * (-self.alpha_c * q * (e - self.e0_cathodic)).exp()
}
pub fn net_current(&self, e: f64) -> f64 {
self.anodic_current(e) - self.cathodic_current(e)
}
pub fn corrosion_potential(&self, e_low: f64, e_high: f64, tol: f64) -> Option<f64> {
let f_low = self.net_current(e_low);
let f_high = self.net_current(e_high);
if f_low * f_high > 0.0 {
return None;
}
let mut lo = e_low;
let mut hi = e_high;
for _ in 0..100 {
let mid = 0.5 * (lo + hi);
if (hi - lo) < tol {
return Some(mid);
}
let f_mid = self.net_current(mid);
if f_mid * self.net_current(lo) <= 0.0 {
hi = mid;
} else {
lo = mid;
}
}
Some(0.5 * (lo + hi))
}
pub fn corrosion_current(&self, e_low: f64, e_high: f64, tol: f64) -> Option<f64> {
let e_corr = self.corrosion_potential(e_low, e_high, tol)?;
Some(self.anodic_current(e_corr))
}
}
pub struct DiffusionLayer {
pub thickness: f64,
pub diffusivity: f64,
pub bulk_concentration: f64,
}
impl DiffusionLayer {
pub fn new(thickness: f64, d: f64, c_bulk: f64) -> Self {
Self {
thickness,
diffusivity: d,
bulk_concentration: c_bulk,
}
}
pub fn limiting_current(&self, n_electrons: u32, f_const: f64, area: f64) -> f64 {
(n_electrons as f64) * f_const * self.diffusivity * self.bulk_concentration * area
/ self.thickness
}
pub fn concentration_at_surface(
&self,
current_density: f64,
n_electrons: u32,
f_const: f64,
) -> f64 {
self.bulk_concentration
- current_density * self.thickness / ((n_electrons as f64) * f_const * self.diffusivity)
}
pub fn diffusion_overpotential(&self, c_surface: f64, _temp: f64, f_over_rt: f64) -> f64 {
(c_surface / self.bulk_concentration).ln() / f_over_rt
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct PeukertModel {
pub capacity_nominal_ah: f64,
pub i_nominal_a: f64,
pub peukert_exponent: f64,
}
impl PeukertModel {
pub fn new(capacity_nominal_ah: f64, i_nominal_a: f64, peukert_exponent: f64) -> Self {
Self {
capacity_nominal_ah,
i_nominal_a,
peukert_exponent,
}
}
pub fn lead_acid_100ah() -> Self {
Self::new(100.0, 5.0, 1.2)
}
pub fn li_ion_10ah() -> Self {
Self::new(10.0, 0.5, 1.05)
}
pub fn peukert_constant(&self) -> f64 {
self.capacity_nominal_ah * self.i_nominal_a.powf(self.peukert_exponent - 1.0)
}
pub fn available_capacity_ah(&self, current_a: f64) -> f64 {
if current_a <= 0.0 {
return self.capacity_nominal_ah;
}
self.capacity_nominal_ah * (self.i_nominal_a / current_a).powf(self.peukert_exponent - 1.0)
}
pub fn discharge_time_h(&self, current_a: f64) -> f64 {
if current_a <= 0.0 {
return f64::INFINITY;
}
self.peukert_constant() / current_a.powf(self.peukert_exponent)
}
pub fn c_rate(&self, current_a: f64) -> f64 {
current_a / self.capacity_nominal_ah
}
pub fn capacity_fade_factor(&self, current_a: f64) -> f64 {
self.available_capacity_ah(current_a) / self.capacity_nominal_ah
}
}
pub struct ButlerVolmerKinetics {
pub i0: f64,
pub alpha_a: f64,
pub alpha_c: f64,
pub temperature: f64,
}
impl ButlerVolmerKinetics {
pub fn new(i0: f64, alpha_a: f64, alpha_c: f64, temp: f64) -> Self {
Self {
i0,
alpha_a,
alpha_c,
temperature: temp,
}
}
pub fn f_over_rt(&self) -> f64 {
FARADAY / (GAS_CONSTANT * self.temperature)
}
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 * (self.alpha_a + self.alpha_c) * FARADAY)
}
pub fn tafel_slope_anodic(&self) -> f64 {
10.0_f64.ln() / (self.alpha_a * self.f_over_rt())
}
pub fn tafel_slope_cathodic(&self) -> f64 {
10.0_f64.ln() / (self.alpha_c * self.f_over_rt())
}
}
#[derive(Debug, Clone)]
pub struct ImpedanceModel {
pub r_solution: f64,
pub r_ct: f64,
pub c_dl: f64,
pub warburg_sigma: f64,
}
impl ImpedanceModel {
pub fn new(r_solution: f64, r_ct: f64, c_dl: f64, warburg_sigma: f64) -> Self {
Self {
r_solution,
r_ct,
c_dl,
warburg_sigma,
}
}
pub fn angular_frequency(f_hz: f64) -> f64 {
2.0 * std::f64::consts::PI * f_hz
}
pub fn z_real(&self, omega: f64) -> f64 {
if omega < 1e-15 {
return self.r_solution + self.r_ct;
}
let sqrt_w = omega.sqrt();
let sigma = self.warburg_sigma;
let zw_r = sigma / sqrt_w;
let rw = self.r_ct + zw_r;
let xc = 1.0 / (omega * self.c_dl);
let _denom = rw * rw + xc * xc;
let z_par_r = rw / (1.0 + (rw / xc).powi(2));
self.r_solution + z_par_r
}
pub fn z_imag(&self, omega: f64) -> f64 {
if omega < 1e-15 {
return 0.0;
}
let sqrt_w = omega.sqrt();
let sigma = self.warburg_sigma;
let zw_r = sigma / sqrt_w;
let zw_i = -sigma / sqrt_w;
let rw = self.r_ct + zw_r;
let xw = zw_i;
let xc = 1.0 / (omega * self.c_dl);
let denom = 1.0 + (rw / xc).powi(2);
(xw - rw * rw / xc - xc) / denom
}
pub fn z_magnitude(&self, omega: f64) -> f64 {
let zr = self.z_real(omega);
let zi = self.z_imag(omega);
(zr * zr + zi * zi).sqrt()
}
pub fn phase_angle(&self, omega: f64) -> f64 {
self.z_imag(omega).atan2(self.z_real(omega))
}
pub fn nyquist_point(&self, f_hz: f64) -> (f64, f64) {
let omega = Self::angular_frequency(f_hz);
(self.z_real(omega), -self.z_imag(omega))
}
}
#[derive(Debug, Clone)]
pub struct ConcentrationPolarisation {
pub c_bulk: f64,
pub j_limiting: f64,
pub n_electrons: u32,
pub temperature: f64,
}
impl ConcentrationPolarisation {
pub fn new(c_bulk: f64, j_limiting: f64, n_electrons: u32, temperature: f64) -> Self {
Self {
c_bulk,
j_limiting,
n_electrons,
temperature,
}
}
pub fn surface_concentration(&self, j: f64) -> f64 {
(self.c_bulk * (1.0 - j / self.j_limiting)).max(0.0)
}
pub fn overpotential(&self, j: f64) -> f64 {
let c_s = self.surface_concentration(j);
if c_s < 1e-15 {
return f64::INFINITY;
}
let rt_over_nf = GAS_CONSTANT * self.temperature / (self.n_electrons as f64 * FARADAY);
-rt_over_nf * (c_s / self.c_bulk).ln()
}
pub fn current_fraction(&self, j: f64) -> f64 {
j / self.j_limiting
}
}
#[derive(Debug, Clone)]
pub struct SeiLayer {
pub r_sei_0: f64,
pub k_sei_ref: f64,
pub t_ref: f64,
pub activation_energy: f64,
}
impl SeiLayer {
pub fn new(r_sei_0: f64, k_sei_ref: f64, t_ref: f64, activation_energy: f64) -> Self {
Self {
r_sei_0,
k_sei_ref,
t_ref,
activation_energy,
}
}
pub fn k_sei(&self, temp_k: f64) -> f64 {
let exponent = -self.activation_energy / GAS_CONSTANT * (1.0 / temp_k - 1.0 / self.t_ref);
self.k_sei_ref * exponent.exp()
}
pub fn resistance(&self, t_s: f64, temp_k: f64) -> f64 {
self.r_sei_0 + self.k_sei(temp_k) * t_s.sqrt()
}
pub fn growth_rate(&self, t_s: f64, temp_k: f64) -> f64 {
if t_s < 1e-15 {
return f64::INFINITY;
}
0.5 * self.k_sei(temp_k) / t_s.sqrt()
}
pub fn time_to_double_resistance(&self, temp_k: f64) -> f64 {
if self.r_sei_0 < f64::EPSILON {
return f64::INFINITY;
}
let k = self.k_sei(temp_k);
(self.r_sei_0 / k).powi(2)
}
}
#[derive(Debug, Clone)]
pub struct Electroplating {
pub molar_mass: f64,
pub valence: u32,
pub current_efficiency: f64,
}
impl Electroplating {
pub fn new(molar_mass: f64, valence: u32, current_efficiency: f64) -> Self {
Self {
molar_mass,
valence,
current_efficiency,
}
}
pub fn mass_deposited_g(&self, current_a: f64, time_s: f64) -> f64 {
self.current_efficiency * current_a * time_s * self.molar_mass
/ (self.valence as f64 * FARADAY)
}
pub fn thickness_um(
&self,
current_a: f64,
time_s: f64,
area_m2: f64,
density_g_cm3: f64,
) -> f64 {
let mass_g = self.mass_deposited_g(current_a, time_s);
let volume_cm3 = mass_g / density_g_cm3;
let area_cm2 = area_m2 * 1.0e4;
let thickness_cm = volume_cm3 / area_cm2;
thickness_cm * 1.0e4
}
pub fn time_for_mass(&self, m_g: f64, current_a: f64) -> f64 {
m_g * self.valence as f64 * FARADAY
/ (self.current_efficiency * current_a * self.molar_mass)
}
pub fn plating_rate_um_per_min(&self, j_a_m2: f64, density_g_cm3: f64) -> f64 {
let j_a_cm2 = j_a_m2 * 1.0e-4;
let rate_g_cm2_s =
self.current_efficiency * j_a_cm2 * self.molar_mass / (self.valence as f64 * FARADAY);
let rate_cm_s = rate_g_cm2_s / density_g_cm3;
rate_cm_s * 1.0e4 * 60.0
}
}
pub struct BatteryCell {
pub capacity_ah: f64,
pub nominal_voltage: f64,
pub internal_resistance: f64,
pub soc: f64,
}
impl BatteryCell {
pub fn new(capacity: f64, voltage: f64, resistance: f64) -> Self {
Self {
capacity_ah: capacity,
nominal_voltage: voltage,
internal_resistance: resistance,
soc: 1.0,
}
}
pub fn open_circuit_voltage(&self) -> f64 {
self.nominal_voltage * (0.8 + 0.2 * self.soc)
}
pub fn terminal_voltage(&self, current_a: f64) -> f64 {
self.open_circuit_voltage() - current_a * self.internal_resistance
}
pub fn discharge(&mut self, current_a: f64, dt_s: f64) {
self.soc -= current_a * dt_s / (self.capacity_ah * 3600.0);
self.soc = self.soc.clamp(0.0, 1.0);
}
pub fn is_depleted(&self) -> bool {
self.soc < 0.05
}
}
#[allow(dead_code)]
pub struct GalvanicSeriesEntry {
pub name: &'static str,
pub potential_v: f64,
}
pub struct ElectrodeKinetics {
pub exchange_current_density: f64,
pub transfer_coefficient_a: f64,
pub transfer_coefficient_c: f64,
pub temperature: f64,
}
impl ElectrodeKinetics {
pub fn new(j0: f64, alpha_a: f64, alpha_c: f64, temp: f64) -> Self {
Self {
exchange_current_density: j0,
transfer_coefficient_a: alpha_a,
transfer_coefficient_c: alpha_c,
temperature: temp,
}
}
pub fn current_density(&self, eta: f64, f_over_rt: f64) -> f64 {
let j0 = self.exchange_current_density;
let aa = self.transfer_coefficient_a;
let ac = self.transfer_coefficient_c;
j0 * ((aa * f_over_rt * eta).exp() - (-ac * f_over_rt * eta).exp())
}
pub fn linearized_resistance(&self, f_over_rt: f64) -> f64 {
1.0 / (self.exchange_current_density * f_over_rt)
}
pub fn tafel_slope_anodic(&self, f_over_rt: f64) -> f64 {
10.0_f64.ln() / (self.transfer_coefficient_a * f_over_rt)
}
}
#[derive(Debug, Clone)]
pub struct GalvanicCouple {
pub e_corr_1: f64,
pub b_a1: f64,
pub i_corr_1: f64,
pub e_corr_2: f64,
pub b_c2: f64,
pub i_corr_2: f64,
}
impl GalvanicCouple {
pub fn new(
e_corr_1: f64,
b_a1: f64,
i_corr_1: f64,
e_corr_2: f64,
b_c2: f64,
i_corr_2: f64,
) -> Self {
Self {
e_corr_1,
b_a1,
i_corr_1,
e_corr_2,
b_c2,
i_corr_2,
}
}
pub fn anodic_current_1(&self, e: f64) -> f64 {
self.i_corr_1 * 10.0_f64.powf((e - self.e_corr_1) / self.b_a1)
}
pub fn cathodic_current_2(&self, e: f64) -> f64 {
self.i_corr_2 * 10.0_f64.powf(-(e - self.e_corr_2) / self.b_c2)
}
pub fn couple_potential(&self, tol: f64) -> Option<f64> {
let e_lo = self.e_corr_1.min(self.e_corr_2) - 0.1;
let e_hi = self.e_corr_1.max(self.e_corr_2) + 0.1;
let f = |e: f64| self.anodic_current_1(e) - self.cathodic_current_2(e);
let fl = f(e_lo);
let fh = f(e_hi);
if fl * fh > 0.0 {
return None;
}
let mut lo = e_lo;
let mut hi = e_hi;
for _ in 0..100 {
let mid = 0.5 * (lo + hi);
if (hi - lo) < tol {
return Some(mid);
}
if f(mid) * f(lo) <= 0.0 {
hi = mid;
} else {
lo = mid;
}
}
Some(0.5 * (lo + hi))
}
pub fn galvanic_current(&self, tol: f64) -> Option<f64> {
let e_couple = self.couple_potential(tol)?;
Some(self.anodic_current_1(e_couple))
}
}
#[derive(Debug, Clone)]
pub struct LiIonDegradation {
pub capacity_init: f64,
pub capacity: f64,
pub resistance_init: f64,
pub resistance: f64,
pub throughput_ah: f64,
pub n_cycles: u64,
pub calendar_days: f64,
}
impl LiIonDegradation {
pub fn new(capacity: f64, resistance: f64) -> Self {
Self {
capacity_init: capacity,
capacity,
resistance_init: resistance,
resistance,
throughput_ah: 0.0,
n_cycles: 0,
calendar_days: 0.0,
}
}
pub fn soh_capacity(&self) -> f64 {
self.capacity / self.capacity_init
}
pub fn soh_resistance(&self) -> f64 {
self.resistance_init / self.resistance
}
pub fn update_cycle_fade(&mut self, delta_ah: f64, k_fade: f64) {
self.throughput_ah += delta_ah;
self.n_cycles += 1;
let fade = k_fade * self.throughput_ah.sqrt();
self.capacity = self.capacity_init * (1.0 - fade).max(0.0);
}
pub fn update_calendar_aging(&mut self, delta_days: f64, k_sei: f64) {
self.calendar_days += delta_days;
self.resistance = self.resistance_init * (1.0 + k_sei * self.calendar_days.sqrt());
}
pub fn is_end_of_life(&self) -> bool {
self.soh_capacity() < 0.8
}
pub fn estimated_remaining_cycles(&self, k_fade: f64) -> Option<u64> {
if k_fade < 1e-30 {
return None;
}
let ah_eol = (0.2 / k_fade).powi(2);
if ah_eol <= self.throughput_ah {
return Some(0);
}
let remaining_ah = ah_eol - self.throughput_ah;
if self.n_cycles == 0 {
return None;
}
let ah_per_cycle = self.throughput_ah / self.n_cycles as f64;
Some((remaining_ah / ah_per_cycle) as u64)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DoubleLayerCapacitance {
pub concentration_mol_m3: f64,
pub permittivity: f64,
pub temperature: f64,
}
impl DoubleLayerCapacitance {
pub fn new(concentration_mol_m3: f64, permittivity: f64, temperature: f64) -> Self {
Self {
concentration_mol_m3,
permittivity,
temperature,
}
}
pub fn kcl_01_mol_l() -> Self {
Self::new(100.0, 78.5 * 8.854e-12, 298.15)
}
pub fn debye_length(&self) -> f64 {
let numerator = self.permittivity * GAS_CONSTANT * self.temperature;
let denominator = 2.0 * self.concentration_mol_m3 * FARADAY * FARADAY;
(numerator / denominator).sqrt()
}
pub fn capacitance_at_pzc(&self) -> f64 {
self.permittivity / self.debye_length()
}
pub fn differential_capacitance(&self, psi: f64) -> f64 {
let kappa = 1.0 / self.debye_length();
let arg = FARADAY * psi / (2.0 * GAS_CONSTANT * self.temperature);
self.permittivity * kappa * arg.cosh()
}
}
#[derive(Debug, Clone)]
pub struct CorrosionRate {
pub i_corr: f64,
pub molar_mass: f64,
pub n_electrons: u32,
pub density_g_cm3: f64,
}
impl CorrosionRate {
pub fn new(i_corr: f64, molar_mass: f64, n_electrons: u32, density_g_cm3: f64) -> Self {
Self {
i_corr,
molar_mass,
n_electrons,
density_g_cm3,
}
}
pub fn mm_per_year(&self) -> f64 {
let i_ua_cm2 = self.i_corr * 0.1;
0.00327 * i_ua_cm2 * self.molar_mass / ((self.n_electrons as f64) * self.density_g_cm3)
}
pub fn from_polarisation_resistance(rp: f64, b_a: f64, b_c: f64) -> f64 {
let b = (b_a * b_c) / (2.303 * (b_a + b_c));
b / rp
}
}
#[derive(Debug, Clone)]
pub struct BatteryModel {
pub capacity_ah: f64,
pub soc: f64,
pub r0: f64,
pub r1: f64,
pub c1: f64,
pub u_rc: f64,
}
impl BatteryModel {
pub fn new(capacity_ah: f64, r0: f64, r1: f64, c1: f64) -> Self {
Self {
capacity_ah,
soc: 1.0,
r0,
r1,
c1,
u_rc: 0.0,
}
}
pub fn open_circuit_voltage(&self) -> f64 {
3.0 + 1.2 * self.soc
}
pub fn terminal_voltage(&self, current_a: f64) -> f64 {
self.open_circuit_voltage() - self.r0 * current_a - self.u_rc
}
pub fn step(&mut self, current_a: f64, dt_s: f64) {
let tau = self.r1 * self.c1;
self.u_rc += (self.r1 * current_a - self.u_rc) / tau * dt_s;
self.soc -= current_a * dt_s / (self.capacity_ah * 3600.0);
self.soc = self.soc.clamp(0.0, 1.0);
}
pub fn is_depleted(&self) -> bool {
self.soc < 0.05
}
pub fn power(&self, current_a: f64) -> f64 {
self.terminal_voltage(current_a) * current_a
}
}
#[derive(Debug, Clone)]
pub struct FuelCellStack {
pub e_rev: f64,
pub i0: f64,
pub tafel_slope: f64,
pub r_ohmic: f64,
pub i_lim: f64,
pub m_conc: f64,
pub n_conc: f64,
}
impl FuelCellStack {
#[allow(clippy::too_many_arguments)]
pub fn new(
e_rev: f64,
i0: f64,
tafel_slope: f64,
r_ohmic: f64,
i_lim: f64,
m_conc: f64,
n_conc: f64,
) -> Self {
Self {
e_rev,
i0,
tafel_slope,
r_ohmic,
i_lim,
m_conc,
n_conc,
}
}
pub fn activation_loss(&self, j: f64) -> f64 {
if j <= 0.0 || j < self.i0 {
return 0.0;
}
self.tafel_slope * (j / self.i0).log10()
}
pub fn ohmic_loss(&self, j: f64) -> f64 {
self.r_ohmic * j
}
pub fn concentration_loss(&self, j: f64) -> f64 {
self.m_conc * (self.n_conc * j).exp()
}
pub fn cell_voltage(&self, j: f64) -> f64 {
if j >= self.i_lim {
return 0.0;
}
let v =
self.e_rev - self.activation_loss(j) - self.ohmic_loss(j) - self.concentration_loss(j);
v.max(0.0)
}
pub fn power_density(&self, j: f64) -> f64 {
j * self.cell_voltage(j)
}
pub fn efficiency(&self, j: f64) -> f64 {
self.cell_voltage(j) / self.e_rev
}
}
#[derive(Debug, Clone)]
pub struct ElectrolyteConductivity {
pub lambda_0: f64,
pub kohlrausch_b: f64,
pub temperature: f64,
}
impl ElectrolyteConductivity {
pub fn new(lambda_0: f64, kohlrausch_b: f64, temperature: f64) -> Self {
Self {
lambda_0,
kohlrausch_b,
temperature,
}
}
pub fn molar_conductivity(&self, conc: f64) -> f64 {
(self.lambda_0 - self.kohlrausch_b * conc.sqrt()).max(0.0)
}
pub fn specific_conductivity(&self, conc: f64) -> f64 {
self.molar_conductivity(conc) * conc
}
pub fn resistivity(&self, conc: f64) -> f64 {
let kappa = self.specific_conductivity(conc);
if kappa < f64::EPSILON {
return f64::INFINITY;
}
1.0 / kappa
}
pub fn temperature_corrected_conductivity(
&self,
kappa_ref: f64,
t_ref: f64,
activation_energy: f64,
) -> f64 {
const R: f64 = 8.314_462_618;
let exponent = -activation_energy / R * (1.0 / self.temperature - 1.0 / t_ref);
kappa_ref * exponent.exp()
}
pub fn transference_number_cation(lambda_plus: f64, lambda_minus: f64) -> f64 {
lambda_plus / (lambda_plus + lambda_minus)
}
}