use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElectrolyteParams {
pub d_e_ref: f64,
pub kappa_ref: f64,
pub t_plus: f64,
pub bruggeman: f64,
pub c_e_init: f64,
pub e_a_de: f64,
pub e_a_kappa: f64,
}
impl ElectrolyteParams {
pub fn lipf6_ec_dmc() -> Self {
Self {
d_e_ref: 7.5e-11, kappa_ref: 1.1, t_plus: 0.364,
bruggeman: 1.5,
c_e_init: 1000.0, e_a_de: 17_000.0,
e_a_kappa: 12_000.0,
}
}
pub fn d_e(&self, temp_k: f64) -> f64 {
const R_GAS: f64 = 8.314;
const T_REF: f64 = 298.15;
self.d_e_ref * (self.e_a_de / R_GAS * (1.0 / T_REF - 1.0 / temp_k)).exp()
}
pub fn kappa(&self, temp_k: f64) -> f64 {
const R_GAS: f64 = 8.314;
const T_REF: f64 = 298.15;
self.kappa_ref * (self.e_a_kappa / R_GAS * (1.0 / T_REF - 1.0 / temp_k)).exp()
}
pub fn d_e_eff(&self, porosity: f64, temp_k: f64) -> f64 {
self.d_e(temp_k) * porosity.powf(self.bruggeman)
}
pub fn kappa_eff(&self, porosity: f64, temp_k: f64) -> f64 {
self.kappa(temp_k) * porosity.powf(self.bruggeman)
}
}
pub struct ElectrolyteState {
pub params: ElectrolyteParams,
pub c_e: Vec<f64>,
pub phi_e: Vec<f64>,
pub n_neg: usize,
pub n_sep: usize,
pub n_pos: usize,
dx_neg: f64,
dx_sep: f64,
dx_pos: f64,
eps_neg: f64,
eps_sep: f64,
eps_pos: f64,
}
impl ElectrolyteState {
#[allow(clippy::too_many_arguments)]
pub fn new(
params: ElectrolyteParams,
l_neg: f64,
l_sep: f64,
l_pos: f64,
eps_neg: f64,
eps_sep: f64,
eps_pos: f64,
n_neg: usize,
n_sep: usize,
n_pos: usize,
) -> Self {
let n_total = n_neg + n_sep + n_pos;
let c_init = params.c_e_init;
Self {
c_e: vec![c_init; n_total],
phi_e: vec![0.0; n_total],
params,
n_neg,
n_sep,
n_pos,
dx_neg: l_neg / n_neg as f64,
dx_sep: l_sep / n_sep as f64,
dx_pos: l_pos / n_pos as f64,
eps_neg,
eps_sep,
eps_pos,
}
}
pub fn n_total(&self) -> usize {
self.n_neg + self.n_sep + self.n_pos
}
pub fn c_avg(&self) -> f64 {
self.c_e.iter().sum::<f64>() / self.c_e.len() as f64
}
pub fn step_concentration(&mut self, j_n_neg: f64, j_n_pos: f64, dt: f64, temp_k: f64) {
let n = self.n_total();
let mut dc = vec![0.0f64; n];
let (d_neg, d_sep, d_pos) = (
self.params.d_e_eff(self.eps_neg, temp_k),
self.params.d_e_eff(self.eps_sep, temp_k),
self.params.d_e_eff(self.eps_pos, temp_k),
);
let src_coeff = 1.0 - self.params.t_plus;
#[allow(clippy::needless_range_loop)]
for i in 0..n {
let (eps, dx, d_eff, src) = if i < self.n_neg {
(self.eps_neg, self.dx_neg, d_neg, src_coeff * j_n_neg)
} else if i < self.n_neg + self.n_sep {
(self.eps_sep, self.dx_sep, d_sep, 0.0)
} else {
(self.eps_pos, self.dx_pos, d_pos, src_coeff * j_n_pos)
};
let c_left = if i == 0 { self.c_e[0] } else { self.c_e[i - 1] };
let c_right = if i == n - 1 {
self.c_e[n - 1]
} else {
self.c_e[i + 1]
};
let d2c = (c_left - 2.0 * self.c_e[i] + c_right) / (dx * dx);
dc[i] = (d_eff * d2c + src) / eps;
}
for (ce, dc_val) in self.c_e.iter_mut().zip(dc.iter()) {
*ce = (*ce + dt * dc_val).max(1e-10);
}
}
pub fn dt_stable(&self, temp_k: f64) -> f64 {
let d_max = self
.params
.d_e_eff(self.eps_neg.min(self.eps_sep).min(self.eps_pos), temp_k);
let dx_min = self.dx_neg.min(self.dx_sep).min(self.dx_pos);
0.5 * dx_min * dx_min / d_max
}
pub fn reset(&mut self) {
let c_init = self.params.c_e_init;
self.c_e.fill(c_init);
self.phi_e.fill(0.0);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_state() -> ElectrolyteState {
let params = ElectrolyteParams::lipf6_ec_dmc();
ElectrolyteState::new(
params, 100e-6, 25e-6, 80e-6, 0.30, 0.40, 0.30, 10, 5, 8, )
}
#[test]
fn test_initial_concentration_uniform() {
let state = default_state();
let c0 = state.params.c_e_init;
for &c in &state.c_e {
assert!((c - c0).abs() < 1e-10);
}
}
#[test]
fn test_n_total() {
let state = default_state();
assert_eq!(state.n_total(), 23);
}
#[test]
fn test_arrhenius_diffusivity() {
let p = ElectrolyteParams::lipf6_ec_dmc();
let d_300 = p.d_e(300.0);
let d_298 = p.d_e(298.15);
assert!(d_300 > d_298, "D_e should increase with temperature");
}
#[test]
fn test_effective_diffusivity_less_than_bulk() {
let p = ElectrolyteParams::lipf6_ec_dmc();
let eps = 0.30;
assert!(p.d_e_eff(eps, 298.15) < p.d_e(298.15));
}
#[test]
fn test_dt_stable_positive() {
let state = default_state();
assert!(state.dt_stable(298.15) > 0.0);
}
#[test]
fn test_step_concentration_conserves_mass() {
let mut state = default_state();
let c_before: f64 = state.c_e.iter().sum();
let dt = state.dt_stable(298.15) * 0.4;
state.step_concentration(0.0, 0.0, dt, 298.15);
let c_after: f64 = state.c_e.iter().sum();
assert!((c_after - c_before).abs() < 1e-6 * c_before.abs() + 1e-10);
}
#[test]
fn test_reset_restores_initial() {
let mut state = default_state();
let dt = state.dt_stable(298.15) * 0.4;
state.step_concentration(1e-4, -1e-4, dt, 298.15);
state.reset();
let c0 = state.params.c_e_init;
for &c in &state.c_e {
assert!((c - c0).abs() < 1e-10);
}
}
}