use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FuelCellError {
InvalidParameter,
OverCurrentLimit,
NegativeVoltage,
}
impl core::fmt::Display for FuelCellError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FuelCellError::InvalidParameter => write!(f, "FuelCellError: invalid parameter"),
FuelCellError::OverCurrentLimit => {
write!(f, "FuelCellError: current exceeds limiting current")
}
FuelCellError::NegativeVoltage => {
write!(f, "FuelCellError: computed voltage is negative")
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PemFuelCell<S: ControlScalar> {
a: S,
i0: S,
r_ohm: S,
b: S,
i_lim: S,
e_nernst: S,
area: S,
current: S,
voltage: S,
}
impl<S: ControlScalar> PemFuelCell<S> {
pub fn new(
a: S,
i0: S,
r_ohm: S,
b: S,
i_lim: S,
e_nernst: S,
area: S,
) -> Result<Self, FuelCellError> {
if a <= S::ZERO
|| i0 <= S::ZERO
|| r_ohm < S::ZERO
|| b <= S::ZERO
|| i_lim <= S::ZERO
|| e_nernst <= S::ZERO
|| area <= S::ZERO
{
return Err(FuelCellError::InvalidParameter);
}
Ok(Self {
a,
i0,
r_ohm,
b,
i_lim,
e_nernst,
area,
current: S::ZERO,
voltage: e_nernst,
})
}
pub fn standard_pemfc() -> Result<Self, FuelCellError> {
Self::new(
S::from_f64(0.06),
S::from_f64(1e-4),
S::from_f64(0.1),
S::from_f64(0.05),
S::from_f64(1.5),
S::from_f64(1.0),
S::from_f64(100.0),
)
}
pub fn set_current(&mut self, i_total: S) -> Result<S, FuelCellError> {
let i_density = i_total / self.area;
if i_density >= self.i_lim {
return Err(FuelCellError::OverCurrentLimit);
}
let i_eff = if i_density < self.i0 {
self.i0
} else {
i_density
};
let v_act = self.a * (i_eff / self.i0).ln();
let v_ohm = i_eff * self.r_ohm;
let ratio = S::ONE - i_eff / self.i_lim;
let v_conc = if ratio > S::EPSILON {
-self.b * ratio.ln()
} else {
self.b * S::from_f64(20.0)
};
let voltage = self.e_nernst - v_act - v_ohm - v_conc;
if voltage < S::ZERO {
return Err(FuelCellError::NegativeVoltage);
}
self.current = i_total;
self.voltage = voltage;
Ok(voltage)
}
#[inline]
pub fn voltage(&self) -> S {
self.voltage
}
#[inline]
pub fn current(&self) -> S {
self.current
}
#[inline]
pub fn power(&self) -> S {
self.current * self.voltage
}
pub fn efficiency(&self) -> S {
if self.e_nernst.abs() < S::EPSILON {
S::ZERO
} else {
self.voltage / self.e_nernst
}
}
#[inline]
pub fn e_nernst(&self) -> S {
self.e_nernst
}
#[inline]
pub fn i_lim(&self) -> S {
self.i_lim
}
#[inline]
pub fn area(&self) -> S {
self.area
}
#[inline]
pub fn max_current(&self) -> S {
self.i_lim * self.area
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_fc() -> PemFuelCell<f64> {
PemFuelCell::standard_pemfc().expect("standard_pemfc construction")
}
#[test]
fn open_circuit_near_nernst() {
let mut fc = make_fc();
let i_total = 1e-4 * 100.0; let v = fc.set_current(i_total).expect("set_current at i0");
assert!(
(v - fc.e_nernst()).abs() < 0.01,
"At i=i0, voltage {v:.4} should be near e_nernst {:.4}",
fc.e_nernst()
);
}
#[test]
fn voltage_decreases_with_current() {
let mut fc = make_fc();
let v_low = fc.set_current(100.0).expect("low current"); let v_high = fc.set_current(50.0).expect("high current"); let mut fc2 = make_fc();
let v_50a = fc2.set_current(50.0).expect("50A");
let mut fc3 = make_fc();
let v_100a = fc3.set_current(100.0).expect("100A");
assert!(
v_100a < v_50a,
"Voltage at 100A ({v_100a:.4}) should be less than at 50A ({v_50a:.4})"
);
let _ = v_low;
let _ = v_high;
}
#[test]
fn over_limit_rejected() {
let mut fc = make_fc();
let res = fc.set_current(151.0);
assert!(
matches!(res, Err(FuelCellError::OverCurrentLimit)),
"Expected OverCurrentLimit, got {res:?}"
);
}
#[test]
fn power_equals_i_times_v() {
let mut fc = make_fc();
let v = fc.set_current(50.0).expect("set_current");
let power = fc.power();
let expected = fc.current() * v;
assert!(
(power - expected).abs() < 1e-10,
"power={power} should equal i*v={expected}"
);
}
#[test]
fn efficiency_less_than_one() {
let mut fc = make_fc();
fc.set_current(50.0).expect("set_current");
let eff = fc.efficiency();
assert!(
eff < 1.0 && eff > 0.0,
"Efficiency {eff} should be in (0, 1)"
);
}
#[test]
fn invalid_params_rejected() {
let res = PemFuelCell::<f64>::new(0.0, 1e-4, 0.1, 0.05, 1.5, 1.0, 100.0);
assert!(matches!(res, Err(FuelCellError::InvalidParameter)));
let res = PemFuelCell::<f64>::new(0.06, 1e-4, 0.1, 0.05, 0.0, 1.0, 100.0);
assert!(matches!(res, Err(FuelCellError::InvalidParameter)));
let res = PemFuelCell::<f64>::new(0.06, 1e-4, 0.1, 0.05, 1.5, 1.0, -5.0);
assert!(matches!(res, Err(FuelCellError::InvalidParameter)));
let res = PemFuelCell::<f64>::new(0.06, 1e-4, 0.1, 0.05, 1.5, 0.0, 100.0);
assert!(matches!(res, Err(FuelCellError::InvalidParameter)));
let res = PemFuelCell::<f64>::new(0.06, 1e-4, -0.1, 0.05, 1.5, 1.0, 100.0);
assert!(matches!(res, Err(FuelCellError::InvalidParameter)));
}
#[test]
fn max_current_equals_i_lim_times_area() {
let fc = make_fc();
let expected = fc.i_lim() * fc.area();
assert!(
(fc.max_current() - expected).abs() < 1e-10,
"max_current={} expected={}",
fc.max_current(),
expected
);
}
#[test]
fn zero_current_gives_nernst_voltage() {
let mut fc = make_fc();
let v = fc.set_current(0.0).expect("zero current");
assert!(
(v - fc.e_nernst()).abs() < 0.05,
"At zero current, voltage {v:.4} should be within 0.05 V of e_nernst {:.4}",
fc.e_nernst()
);
}
}