#![allow(dead_code)]
#![allow(unused_imports)]
#![allow(clippy::too_many_arguments)]
use std::f64::consts::PI;
const R: f64 = 8.314;
const F_FARADAY: f64 = 96_485.0;
const K_B: f64 = 1.380649e-23;
const Q_E: f64 = 1.602176634e-19;
const H_PLANCK: f64 = 6.62607015e-34;
const C_LIGHT: f64 = 2.99792458e8;
#[derive(Clone, Debug)]
pub struct LiIonElectrode {
pub i0: f64,
pub alpha: f64,
pub temperature: f64,
pub thickness: f64,
pub d_solid: f64,
pub d_electrolyte: f64,
pub cs_max: f64,
pub epsilon_s: f64,
pub particle_radius: f64,
pub ocp_a: f64,
pub ocp_b: f64,
}
impl LiIonElectrode {
pub fn lco_cathode() -> Self {
Self {
i0: 2.0,
alpha: 0.5,
temperature: 298.15,
thickness: 80.0e-6,
d_solid: 1.0e-14,
d_electrolyte: 7.5e-10,
cs_max: 51_410.0,
epsilon_s: 0.5,
particle_radius: 2.0e-6,
ocp_a: 4.2,
ocp_b: -0.5,
}
}
pub fn graphite_anode() -> Self {
Self {
i0: 3.6,
alpha: 0.5,
temperature: 298.15,
thickness: 100.0e-6,
d_solid: 3.9e-14,
d_electrolyte: 7.5e-10,
cs_max: 26_390.0,
epsilon_s: 0.471,
particle_radius: 5.0e-6,
ocp_a: 0.2,
ocp_b: 0.1,
}
}
pub fn butler_volmer(&self, eta: f64) -> f64 {
let f_rt = F_FARADAY / (R * self.temperature);
self.i0 * ((self.alpha * f_rt * eta).exp() - ((self.alpha - 1.0) * f_rt * eta).exp())
}
pub fn linear_butler_volmer(&self, eta: f64) -> f64 {
self.i0 * F_FARADAY / (R * self.temperature) * eta
}
pub fn ocp(&self, x: f64) -> f64 {
self.ocp_a + self.ocp_b * x
}
pub fn diffusion_time_constant(&self) -> f64 {
self.particle_radius * self.particle_radius / (15.0 * self.d_solid)
}
pub fn specific_interfacial_area(&self) -> f64 {
3.0 * self.epsilon_s / self.particle_radius
}
pub fn areal_capacity(&self) -> f64 {
self.cs_max * F_FARADAY * self.epsilon_s * self.thickness
}
pub fn state_of_charge(&self, cs_surf: f64) -> f64 {
(cs_surf / self.cs_max).clamp(0.0, 1.0)
}
pub fn tafel_slope(&self) -> f64 {
R * self.temperature / (self.alpha * F_FARADAY) * 10.0f64.ln()
}
}
#[derive(Clone, Debug)]
pub struct LiIonCell {
pub cathode: LiIonElectrode,
pub anode: LiIonElectrode,
pub r_electrolyte: f64,
pub r_contact: f64,
pub capacity_ah: f64,
}
impl LiIonCell {
pub fn nmc18650() -> Self {
Self {
cathode: LiIonElectrode::lco_cathode(),
anode: LiIonElectrode::graphite_anode(),
r_electrolyte: 5.0e-5,
r_contact: 5.0e-6,
capacity_ah: 2.5,
}
}
pub fn ocv(&self, soc_cathode: f64, soc_anode: f64) -> f64 {
self.cathode.ocp(soc_cathode) - self.anode.ocp(soc_anode)
}
pub fn terminal_voltage(&self, j: f64, soc: f64) -> f64 {
let ocv = self.cathode.ocp(soc) - self.anode.ocp(1.0 - soc);
let eta_c = self.cathode.butler_volmer(j / self.cathode.i0);
let eta_a = self.anode.butler_volmer(-j / self.anode.i0);
let v_ir = j * (self.r_electrolyte + self.r_contact);
ocv - eta_c + eta_a - v_ir
}
pub fn c_rate(&self, current_a: f64) -> f64 {
current_a / self.capacity_ah
}
pub fn ragone_point(&self, c_rate: f64, cell_mass_kg: f64) -> (f64, f64) {
let current = c_rate * self.capacity_ah;
let voltage = 3.6; let power = current * voltage;
let energy = self.capacity_ah * voltage;
(energy / cell_mass_kg, power / cell_mass_kg)
}
}
#[derive(Clone, Debug)]
pub struct NafionMembrane {
pub thickness: f64,
pub water_content: f64,
pub temperature: f64,
pub eq_weight: f64,
pub dry_density: f64,
}
impl NafionMembrane {
pub fn nafion117() -> Self {
Self {
thickness: 183.0e-6,
water_content: 14.0,
temperature: 353.15,
eq_weight: 1100.0,
dry_density: 2100.0,
}
}
pub fn proton_conductivity(&self) -> f64 {
let lambda = self.water_content;
let t = self.temperature;
(0.005139 * lambda - 0.00326) * (1268.0 * (1.0 / 303.15 - 1.0 / t)).exp() * 1000.0 }
pub fn area_specific_resistance(&self) -> f64 {
self.thickness / self.proton_conductivity()
}
pub fn water_diffusivity(&self) -> f64 {
let lambda = self.water_content;
let t = self.temperature;
let d_w = if lambda < 3.0 {
1e-10 * (0.87 * (3.0 - lambda) + 2.95 * lambda)
} else if lambda < 17.0 {
1e-10 * (2.95 + 1.00 * (lambda - 3.0))
} else {
1e-10 * (1.54 + 0.198 * lambda)
};
d_w * (2416.0 * (1.0 / 303.15 - 1.0 / t)).exp()
}
pub fn sorption_isotherm(&self, a_w: f64) -> f64 {
0.043 + 17.81 * a_w - 39.85 * a_w * a_w + 36.0 * a_w * a_w * a_w
}
pub fn electroosmotic_drag(&self) -> f64 {
2.5 * self.water_content / 22.0
}
}
#[derive(Clone, Debug)]
pub struct GasDiffusionLayer {
pub porosity: f64,
pub tortuosity: f64,
pub thickness: f64,
pub contact_angle: f64,
pub permeability: f64,
}
impl GasDiffusionLayer {
pub fn toray_tgp_h120() -> Self {
Self {
porosity: 0.78,
tortuosity: 1.5,
thickness: 370.0e-6,
contact_angle: 110.0f64.to_radians(),
permeability: 7.5e-12,
}
}
pub fn effective_diffusivity(&self, d_bulk: f64) -> f64 {
d_bulk * self.porosity.powf(1.5)
}
pub fn knudsen_diffusivity(&self, d_pore: f64, m_molar: f64, temperature: f64) -> f64 {
(d_pore / 3.0) * (8.0 * R * temperature / (PI * m_molar)).sqrt()
}
pub fn darcy_velocity(&self, viscosity: f64, dp_dx: f64) -> f64 {
self.permeability / viscosity * dp_dx
}
pub fn capillary_pressure(&self, d_pore: f64, surface_tension: f64) -> f64 {
4.0 * surface_tension * self.contact_angle.cos().abs() / d_pore
}
}
#[derive(Clone, Debug)]
pub struct PemFuelCell {
pub membrane: NafionMembrane,
pub cathode_gdl: GasDiffusionLayer,
pub anode_gdl: GasDiffusionLayer,
pub i0_c: f64,
pub i0_a: f64,
pub alpha_c: f64,
pub e_nernst: f64,
pub temperature: f64,
}
impl PemFuelCell {
pub fn standard() -> Self {
Self {
membrane: NafionMembrane::nafion117(),
cathode_gdl: GasDiffusionLayer::toray_tgp_h120(),
anode_gdl: GasDiffusionLayer::toray_tgp_h120(),
i0_c: 1.0e-4,
i0_a: 10.0,
alpha_c: 0.5,
e_nernst: 1.229,
temperature: 353.15,
}
}
pub fn cathode_activation_overpotential(&self, j: f64) -> f64 {
R * self.temperature / (self.alpha_c * F_FARADAY) * (j / self.i0_c).abs().ln()
}
pub fn ohmic_overpotential(&self, j: f64) -> f64 {
j * self.membrane.area_specific_resistance()
}
pub fn limiting_current_density(&self, c_bulk: f64, d_eff: f64) -> f64 {
4.0 * F_FARADAY * d_eff * c_bulk / self.cathode_gdl.thickness
}
pub fn cell_voltage(&self, j: f64) -> f64 {
let eta_act = self.cathode_activation_overpotential(j);
let eta_ohm = self.ohmic_overpotential(j);
(self.e_nernst - eta_act - eta_ohm).max(0.0)
}
pub fn power_density(&self, j: f64) -> f64 {
j * self.cell_voltage(j)
}
}
#[derive(Clone, Debug)]
pub struct SolarCell {
pub j_sc: f64,
pub j0: f64,
pub ideality: f64,
pub r_series: f64,
pub r_shunt: f64,
pub temperature: f64,
pub bandgap_ev: f64,
}
impl SolarCell {
pub fn new(j_sc: f64, j0: f64, ideality: f64, temperature: f64, r_series: f64) -> Self {
Self {
j_sc,
j0,
ideality,
r_series,
r_shunt: 1.0e4,
temperature,
bandgap_ev: 1.12,
}
}
pub fn silicon() -> Self {
Self {
j_sc: 400.0,
j0: 1.0e-10,
ideality: 1.0,
r_series: 1.0e-5,
r_shunt: 1.0e3,
temperature: 298.15,
bandgap_ev: 1.12,
}
}
pub fn gaas() -> Self {
Self {
j_sc: 290.0,
j0: 1.0e-15,
ideality: 1.0,
r_series: 5.0e-6,
r_shunt: 5.0e3,
temperature: 298.15,
bandgap_ev: 1.42,
}
}
pub fn thermal_voltage(&self) -> f64 {
K_B * self.temperature / Q_E
}
pub fn current_density(&self, v: f64) -> f64 {
let vt = self.thermal_voltage() * self.ideality;
let j_diode = self.j0 * ((v / vt).exp() - 1.0);
let j_shunt = v / self.r_shunt;
self.j_sc - j_diode - j_shunt
}
pub fn voc(&self) -> f64 {
let vt = self.thermal_voltage() * self.ideality;
vt * (self.j_sc / self.j0 + 1.0).ln()
}
pub fn fill_factor(&self) -> f64 {
let voc_norm = self.voc() / (self.thermal_voltage() * self.ideality);
(voc_norm - (voc_norm + 0.72).ln()) / (voc_norm + 1.0)
}
pub fn v_mpp(&self) -> f64 {
let voc = self.voc();
let vt = self.thermal_voltage() * self.ideality;
let mut v = voc * 0.8;
for _ in 0..20 {
v = voc - vt * (v / vt + 1.0).ln();
}
v
}
pub fn p_max(&self) -> f64 {
let vmpp = self.v_mpp();
let jmpp = self.current_density(vmpp);
vmpp * jmpp.max(0.0)
}
pub fn efficiency(&self, irradiance: f64) -> f64 {
self.p_max() / irradiance
}
pub fn sq_limit(bandgap_ev: f64) -> f64 {
let eg = bandgap_ev;
if !(0.5..=4.0).contains(&eg) {
return 0.0;
}
0.31 * (1.0 - ((eg - 1.34) / 0.8).powi(2)).max(0.0)
}
pub fn temp_coeff_voc(&self) -> f64 {
-K_B / Q_E * (self.j_sc / self.j0).ln() / self.temperature
}
}
#[derive(Clone, Debug)]
pub struct Thermoelectric {
pub seebeck: f64,
pub electrical_conductivity: f64,
pub thermal_conductivity: f64,
pub temperature: f64,
}
impl Thermoelectric {
pub fn bi2te3() -> Self {
Self {
seebeck: 200.0e-6,
electrical_conductivity: 1.0e5,
thermal_conductivity: 1.0,
temperature: 300.0,
}
}
pub fn pbte_high_temp() -> Self {
Self {
seebeck: 300.0e-6,
electrical_conductivity: 7.0e4,
thermal_conductivity: 1.5,
temperature: 750.0,
}
}
pub fn zt(&self) -> f64 {
self.seebeck * self.seebeck * self.electrical_conductivity * self.temperature
/ self.thermal_conductivity
}
pub fn power_factor(&self) -> f64 {
self.seebeck * self.seebeck * self.electrical_conductivity
}
pub fn carnot_efficiency(&self, t_cold: f64) -> f64 {
1.0 - t_cold / self.temperature
}
pub fn max_efficiency(&self, t_hot: f64, t_cold: f64) -> f64 {
let zt_avg = self.zt(); let sqrt_term = (1.0 + zt_avg).sqrt();
let delta_t = t_hot - t_cold;
(delta_t / t_hot) * (sqrt_term - 1.0) / (sqrt_term + t_cold / t_hot)
}
pub fn peltier_coefficient(&self) -> f64 {
self.seebeck * self.temperature
}
pub fn thomson_coefficient(&self, ds_dt: f64) -> f64 {
self.temperature * ds_dt
}
pub fn max_cop_cooling(&self, t_hot: f64, t_cold: f64) -> f64 {
let zt_avg = self.zt();
let sqrt_term = (1.0 + zt_avg).sqrt();
let delta_t = t_hot - t_cold;
(t_cold * (sqrt_term - t_hot / t_cold)) / (delta_t * (sqrt_term + 1.0))
}
pub fn seebeck_voltage(&self, dt: f64) -> f64 {
self.seebeck * dt
}
pub fn thermal_resistance(&self, length: f64) -> f64 {
length / self.thermal_conductivity
}
}
#[derive(Clone, Debug)]
pub struct Supercapacitor {
pub c_specific: f64,
pub electrode_area: f64,
pub r_esr: f64,
pub r_leakage: f64,
pub v_max: f64,
pub pseudo_fraction: f64,
pub temperature: f64,
}
impl Supercapacitor {
pub fn activated_carbon_500f() -> Self {
Self {
c_specific: 0.2,
electrode_area: 2500.0,
r_esr: 1.0e-3,
r_leakage: 1.0e5,
v_max: 2.7,
pseudo_fraction: 0.0,
temperature: 298.15,
}
}
pub fn ruo2_pseudocap() -> Self {
Self {
c_specific: 0.8,
electrode_area: 500.0,
r_esr: 5.0e-3,
r_leakage: 5.0e4,
v_max: 1.2,
pseudo_fraction: 0.6,
temperature: 298.15,
}
}
pub fn capacitance(&self) -> f64 {
self.c_specific * self.electrode_area * (1.0 + self.pseudo_fraction)
}
pub fn energy_stored(&self, v: f64) -> f64 {
0.5 * self.capacitance() * v * v
}
pub fn max_energy(&self) -> f64 {
self.energy_stored(self.v_max)
}
pub fn peak_power(&self) -> f64 {
self.v_max * self.v_max / (4.0 * self.r_esr)
}
pub fn specific_energy_wh_kg(&self, m_kg: f64) -> f64 {
self.max_energy() / (m_kg * 3600.0)
}
pub fn time_constant(&self) -> f64 {
self.r_esr * self.capacitance()
}
pub fn voltage_decay(&self, v0: f64, t: f64) -> f64 {
let tau_leak = self.r_leakage * self.capacitance();
v0 * (-t / tau_leak).exp()
}
pub fn charge_stored(&self, v: f64) -> f64 {
self.capacitance() * v
}
pub fn helmholtz_capacitance(epsilon_r: f64, d_debye: f64) -> f64 {
8.854187817e-12 * epsilon_r / d_debye
}
pub fn debye_length(c_mol_l: f64, z: f64, temperature: f64) -> f64 {
let c_mol_m3 = c_mol_l * 1000.0;
let eps = 8.854e-12 * 80.0; (eps * R * temperature / (2.0 * F_FARADAY * F_FARADAY * c_mol_m3 * z * z)).sqrt()
}
}
#[derive(Clone, Debug)]
pub struct MetalHydride {
pub max_capacity_wt: f64,
pub ea_absorption: f64,
pub ea_desorption: f64,
pub k0_absorption: f64,
pub k0_desorption: f64,
pub p_eq_298: f64,
pub delta_h: f64,
pub delta_s: f64,
pub temperature: f64,
}
impl MetalHydride {
pub fn lani5() -> Self {
Self {
max_capacity_wt: 1.4,
ea_absorption: 21_000.0,
ea_desorption: 35_000.0,
k0_absorption: 500.0,
k0_desorption: 200.0,
p_eq_298: 200_000.0,
delta_h: -30_800.0,
delta_s: -108.0,
temperature: 298.15,
}
}
pub fn mgh2() -> Self {
Self {
max_capacity_wt: 7.6,
ea_absorption: 60_000.0,
ea_desorption: 80_000.0,
k0_absorption: 100.0,
k0_desorption: 50.0,
p_eq_298: 0.01,
delta_h: -75_000.0,
delta_s: -135.0,
temperature: 623.15,
}
}
pub fn equilibrium_pressure(&self, t: f64) -> f64 {
let p0 = 1.0e5; p0 * (self.delta_h / (R * t) - self.delta_s / R).exp()
}
pub fn absorption_rate_constant(&self, t: f64) -> f64 {
self.k0_absorption * (-self.ea_absorption / (R * t)).exp()
}
pub fn desorption_rate_constant(&self, t: f64) -> f64 {
self.k0_desorption * (-self.ea_desorption / (R * t)).exp()
}
pub fn absorption_rate(&self, x: f64, p: f64) -> f64 {
let peq = self.equilibrium_pressure(self.temperature);
let ka = self.absorption_rate_constant(self.temperature);
if p <= peq {
return 0.0;
}
ka * (self.max_capacity_wt - x) * ((p / peq).ln()).max(0.0)
}
pub fn desorption_rate(&self, x: f64, p: f64) -> f64 {
let peq = self.equilibrium_pressure(self.temperature);
let kd = self.desorption_rate_constant(self.temperature);
if p >= peq {
return 0.0;
}
kd * x * ((peq / p.max(1e-10)).ln()).max(0.0)
}
pub fn absorption_heat(&self, rate_wt_per_s: f64) -> f64 {
rate_wt_per_s * (-self.delta_h) / 2.016 }
}
#[derive(Clone, Debug)]
pub struct NuclearMaterial {
pub name: String,
pub e_displacement: f64,
pub swelling_rate: f64,
pub hardening_coeff: f64,
pub creep_coefficient: f64,
pub dbtt_shift_per_dpa: f64,
pub yield_stress_0: f64,
pub modulus: f64,
}
impl NuclearMaterial {
pub fn f82h() -> Self {
Self {
name: "F82H ferritic-martensitic steel".into(),
e_displacement: 40.0,
swelling_rate: 0.002,
hardening_coeff: 120.0e6,
creep_coefficient: 1.0e-6,
dbtt_shift_per_dpa: 1.5,
yield_stress_0: 500.0e6,
modulus: 210.0e9,
}
}
pub fn ss316l() -> Self {
Self {
name: "316L stainless steel".into(),
e_displacement: 40.0,
swelling_rate: 0.05,
hardening_coeff: 200.0e6,
creep_coefficient: 2.0e-6,
dbtt_shift_per_dpa: 0.0,
yield_stress_0: 270.0e6,
modulus: 193.0e9,
}
}
pub fn dpa_from_fluence(&self, fluence: f64, sigma_d: f64) -> f64 {
fluence * sigma_d
}
pub fn swelling(&self, dpa: f64) -> f64 {
if dpa < 1.0 {
return 0.0;
}
self.swelling_rate * (dpa - 1.0) / 100.0
}
pub fn irradiation_hardening(&self, dpa: f64) -> f64 {
self.hardening_coeff * dpa.sqrt()
}
pub fn irradiated_yield_stress(&self, dpa: f64) -> f64 {
self.yield_stress_0 + self.irradiation_hardening(dpa)
}
pub fn irradiation_creep_rate(&self, sigma: f64, dpas_per_s: f64) -> f64 {
self.creep_coefficient * sigma * dpas_per_s
}
pub fn dbtt_shift(&self, dpa: f64) -> f64 {
self.dbtt_shift_per_dpa * dpa
}
pub fn pka_lindhard(&self, e_recoil: f64, atomic_mass: f64) -> f64 {
let xi = 0.16 * atomic_mass.powf(-2.0 / 3.0);
e_recoil / (1.0 + xi * e_recoil / self.e_displacement)
}
pub fn nrt_displacements(&self, e_pka: f64) -> f64 {
if e_pka < self.e_displacement {
return 0.0;
}
0.8 * e_pka / (2.0 * self.e_displacement)
}
}
#[derive(Clone, Debug)]
pub struct PiezoHarvester {
pub d31: f64,
pub k31: f64,
pub e_piezo: f64,
pub density: f64,
pub length: f64,
pub width: f64,
pub thickness: f64,
pub permittivity: f64,
pub damping: f64,
pub proof_mass: f64,
}
impl PiezoHarvester {
pub fn pzt5a() -> Self {
Self {
d31: -175.0e-12,
k31: 0.34,
e_piezo: 61.0e9,
density: 7750.0,
length: 30.0e-3,
width: 5.0e-3,
thickness: 0.5e-3,
permittivity: 15.0e-9,
damping: 0.02,
proof_mass: 1.0e-3,
}
}
pub fn natural_frequency(&self) -> f64 {
let ei = self.e_piezo * self.width * self.thickness.powi(3) / 12.0;
let m = self.proof_mass + 0.23 * self.density * self.width * self.thickness * self.length;
let k = 3.0 * ei / self.length.powi(3);
(k / m).sqrt() / (2.0 * PI)
}
pub fn open_circuit_voltage(&self, a: f64, f: f64) -> f64 {
let fn_ = self.natural_frequency();
let omega = 2.0 * PI * f;
let omega_n = 2.0 * PI * fn_;
let r = omega / omega_n;
let m = self.proof_mass;
let denom = ((1.0 - r * r).powi(2) + (2.0 * self.damping * r).powi(2)).sqrt();
let x_amp =
m * a / (self.e_piezo * self.width * self.thickness * denom / self.length.powi(2));
let h_pc = self.e_piezo * self.d31.abs() * 3.0 * self.thickness
/ (2.0 * self.length * self.length);
let c_p = self.permittivity * self.width * self.length / self.thickness;
h_pc * x_amp / c_p
}
pub fn max_power(&self, a: f64) -> f64 {
let fn_ = self.natural_frequency();
let voc = self.open_circuit_voltage(a, fn_);
let c_p = self.permittivity * self.width * self.length / self.thickness;
let omega_n = 2.0 * PI * fn_;
let r_opt = 1.0 / (omega_n * c_p);
voc * voc / (4.0 * r_opt)
}
pub fn power_density(&self, a: f64) -> f64 {
let vol = self.length * self.width * self.thickness;
self.max_power(a) / vol
}
pub fn g31(&self) -> f64 {
self.d31 / self.permittivity
}
pub fn quality_factor(&self) -> f64 {
1.0 / (2.0 * self.damping)
}
pub fn coupling_efficiency(&self) -> f64 {
let k2 = self.k31 * self.k31;
k2 / (1.0 + k2)
}
}
#[derive(Clone, Debug)]
pub struct CapacityFadingModel {
pub q0: f64,
pub k_sei: f64,
pub k_cal: f64,
pub cycle_exp: f64,
}
impl CapacityFadingModel {
pub fn nmc18650() -> Self {
Self {
q0: 3.0,
k_sei: 1.5e-4,
k_cal: 1.2e-4,
cycle_exp: 0.5,
}
}
pub fn capacity_after_cycles(&self, n_cycles: f64) -> f64 {
self.q0 * (1.0 - self.k_cal * n_cycles.powf(self.cycle_exp))
}
pub fn capacity_calendar(&self, t_hours: f64) -> f64 {
self.q0 * (1.0 - self.k_sei * t_hours.sqrt())
}
pub fn state_of_health(&self, q_current: f64) -> f64 {
q_current / self.q0
}
pub fn cycle_life_80pct(&self) -> f64 {
let fade_target = 0.20;
(fade_target / self.k_cal).powf(1.0 / self.cycle_exp)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_butler_volmer_zero_eta() {
let e = LiIonElectrode::lco_cathode();
let i = e.butler_volmer(0.0);
assert!(i.abs() < 1e-10, "BV at zero overpotential should be ~0");
}
#[test]
fn test_butler_volmer_positive_eta() {
let e = LiIonElectrode::lco_cathode();
let i = e.butler_volmer(0.05);
assert!(
i > 0.0,
"Positive overpotential should give positive current"
);
}
#[test]
fn test_diffusion_time_constant() {
let e = LiIonElectrode::graphite_anode();
let tau = e.diffusion_time_constant();
assert!(tau > 0.0);
let expected = 5.0e-6_f64.powi(2) / (15.0 * 3.9e-14);
assert!(approx_eq(tau, expected, 1e-5));
}
#[test]
fn test_areal_capacity_positive() {
let e = LiIonElectrode::lco_cathode();
let q = e.areal_capacity();
assert!(q > 0.0);
}
#[test]
fn test_tafel_slope_positive() {
let e = LiIonElectrode::lco_cathode();
assert!(e.tafel_slope() > 0.0);
}
#[test]
fn test_nafion_conductivity_positive() {
let m = NafionMembrane::nafion117();
let sigma = m.proton_conductivity();
assert!(sigma > 0.0);
}
#[test]
fn test_nafion_asr_positive() {
let m = NafionMembrane::nafion117();
let asr = m.area_specific_resistance();
assert!(asr > 0.0);
}
#[test]
fn test_nafion_sorption_isotherm() {
let m = NafionMembrane::nafion117();
let lambda_0 = m.sorption_isotherm(0.0);
let lambda_1 = m.sorption_isotherm(1.0);
assert!(lambda_0 >= 0.0);
assert!(lambda_1 > lambda_0);
}
#[test]
fn test_gdl_effective_diffusivity() {
let gdl = GasDiffusionLayer::toray_tgp_h120();
let d_eff = gdl.effective_diffusivity(2.6e-5);
assert!(d_eff < 2.6e-5 && d_eff > 0.0);
}
#[test]
fn test_gdl_darcy_velocity() {
let gdl = GasDiffusionLayer::toray_tgp_h120();
let v = gdl.darcy_velocity(1e-5, 1000.0);
assert!(v > 0.0);
}
#[test]
fn test_solar_cell_voc_positive() {
let cell = SolarCell::silicon();
let voc = cell.voc();
assert!(voc > 0.5 && voc < 1.0);
}
#[test]
fn test_solar_cell_fill_factor_range() {
let cell = SolarCell::silicon();
let ff = cell.fill_factor();
assert!(ff > 0.5 && ff < 0.99);
}
#[test]
fn test_solar_cell_efficiency() {
let cell = SolarCell::silicon();
let eta = cell.efficiency(1000.0);
assert!(eta > 0.0 && eta < 0.5);
}
#[test]
fn test_sq_limit_silicon() {
let eta = SolarCell::sq_limit(1.12);
assert!(eta > 0.0);
}
#[test]
fn test_solar_temp_coeff_negative() {
let cell = SolarCell::silicon();
assert!(cell.temp_coeff_voc() < 0.0);
}
#[test]
fn test_zt_bi2te3() {
let te = Thermoelectric::bi2te3();
let zt = te.zt();
assert!(zt > 0.5 && zt < 2.0);
}
#[test]
fn test_power_factor_positive() {
let te = Thermoelectric::bi2te3();
assert!(te.power_factor() > 0.0);
}
#[test]
fn test_peltier_coefficient() {
let te = Thermoelectric::bi2te3();
let pi = te.peltier_coefficient();
assert!(approx_eq(pi, te.seebeck * te.temperature, 1e-20));
}
#[test]
fn test_seebeck_voltage() {
let te = Thermoelectric::bi2te3();
let v = te.seebeck_voltage(10.0);
assert!(approx_eq(v, te.seebeck * 10.0, 1e-20));
}
#[test]
fn test_max_efficiency_positive() {
let te = Thermoelectric::bi2te3();
let eta = te.max_efficiency(350.0, 250.0);
assert!(eta > 0.0 && eta < 1.0);
}
#[test]
fn test_supercap_capacitance() {
let sc = Supercapacitor::activated_carbon_500f();
let c = sc.capacitance();
assert!(approx_eq(c, sc.c_specific * sc.electrode_area, 1.0));
}
#[test]
fn test_supercap_energy_stored() {
let sc = Supercapacitor::activated_carbon_500f();
let e = sc.energy_stored(1.0);
assert!(approx_eq(e, 0.5 * sc.capacitance() * 1.0, 1.0));
}
#[test]
fn test_supercap_peak_power() {
let sc = Supercapacitor::activated_carbon_500f();
let p = sc.peak_power();
assert!(p > 0.0);
}
#[test]
fn test_supercap_voltage_decay() {
let sc = Supercapacitor::activated_carbon_500f();
let v0 = 2.7;
let v1 = sc.voltage_decay(v0, 3600.0);
assert!(v1 < v0);
}
#[test]
fn test_debye_length_dilute() {
let ld = Supercapacitor::debye_length(0.001, 1.0, 298.15);
assert!(ld > 1e-9 && ld < 1e-6);
}
#[test]
fn test_lani5_eq_pressure() {
let mh = MetalHydride::lani5();
let peq = mh.equilibrium_pressure(298.15);
assert!(peq > 0.0);
}
#[test]
fn test_absorption_rate_zero_below_eq() {
let mh = MetalHydride::lani5();
let peq = mh.equilibrium_pressure(mh.temperature);
let rate = mh.absorption_rate(0.0, peq * 0.5);
assert!(rate == 0.0);
}
#[test]
fn test_absorption_rate_positive_above_eq() {
let mh = MetalHydride::lani5();
let peq = mh.equilibrium_pressure(mh.temperature);
let rate = mh.absorption_rate(0.0, peq * 2.0);
assert!(rate > 0.0);
}
#[test]
fn test_swelling_zero_at_low_dpa() {
let nm = NuclearMaterial::f82h();
assert_eq!(nm.swelling(0.5), 0.0);
}
#[test]
fn test_swelling_positive_above_threshold() {
let nm = NuclearMaterial::f82h();
let sw = nm.swelling(10.0);
assert!(sw > 0.0);
}
#[test]
fn test_irradiation_hardening_increases() {
let nm = NuclearMaterial::f82h();
let h1 = nm.irradiation_hardening(1.0);
let h10 = nm.irradiation_hardening(10.0);
assert!(h10 > h1);
}
#[test]
fn test_irradiated_yield_stress() {
let nm = NuclearMaterial::ss316l();
let sy = nm.irradiated_yield_stress(5.0);
assert!(sy > nm.yield_stress_0);
}
#[test]
fn test_nrt_displacements_zero_below_threshold() {
let nm = NuclearMaterial::f82h();
assert_eq!(nm.nrt_displacements(nm.e_displacement - 1.0), 0.0);
}
#[test]
fn test_piezo_natural_frequency() {
let ph = PiezoHarvester::pzt5a();
let fn_ = ph.natural_frequency();
assert!(fn_ > 0.0 && fn_ < 10000.0);
}
#[test]
fn test_piezo_quality_factor() {
let ph = PiezoHarvester::pzt5a();
let qm = ph.quality_factor();
assert!(approx_eq(qm, 25.0, 1e-10));
}
#[test]
fn test_piezo_coupling_efficiency() {
let ph = PiezoHarvester::pzt5a();
let eta = ph.coupling_efficiency();
assert!(eta > 0.0 && eta < 1.0);
}
#[test]
fn test_capacity_fading_decreases() {
let m = CapacityFadingModel::nmc18650();
let q100 = m.capacity_after_cycles(100.0);
let q500 = m.capacity_after_cycles(500.0);
assert!(q100 > q500);
}
#[test]
fn test_capacity_soh_at_start() {
let m = CapacityFadingModel::nmc18650();
let soh = m.state_of_health(m.q0);
assert!(approx_eq(soh, 1.0, 1e-10));
}
#[test]
fn test_cycle_life_positive() {
let m = CapacityFadingModel::nmc18650();
let life = m.cycle_life_80pct();
assert!(life > 0.0);
}
#[test]
fn test_cell_ocv_positive() {
let cell = LiIonCell::nmc18650();
let ocv = cell.ocv(0.5, 0.5);
assert!(ocv.abs() < 5.0);
}
#[test]
fn test_pem_fuel_cell_voltage() {
let fc = PemFuelCell::standard();
let v = fc.cell_voltage(100.0);
assert!((0.0..=1.3).contains(&v));
}
#[test]
fn test_pem_power_density() {
let fc = PemFuelCell::standard();
let p = fc.power_density(1000.0);
assert!(p > 0.0);
}
}