use super::electrolyzer::H2_HHV_KWH_PER_KG;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FuelCellType {
Pemfc,
Sofc,
Mcfc,
}
const H2O_PER_H2_KG: f64 = 9.0;
#[derive(Debug, Clone)]
pub struct FuelCell {
pub rated_power_mw: f64,
pub fuel_cell_type: FuelCellType,
pub min_power_fraction: f64,
pub efficiency_curve: Vec<(f64, f64)>,
pub heat_recovery_fraction: f64,
pub startup_time_min: f64,
pub degradation_pct_per_kh: f64,
pub operating_hours: f64,
pub parasitic_fraction: f64,
}
impl FuelCell {
pub fn new(rated_mw: f64, fc_type: FuelCellType) -> Self {
let (min_frac, curve, heat_rec, startup, deg_rate, parasitic) = match fc_type {
FuelCellType::Pemfc => (
0.10,
vec![
(0.10, 0.52),
(0.25, 0.58),
(0.50, 0.60),
(0.75, 0.57),
(1.00, 0.53),
],
0.35, 5.0, 1.0, 0.03, ),
FuelCellType::Sofc => (
0.15,
vec![
(0.15, 0.55),
(0.30, 0.62),
(0.60, 0.65),
(0.80, 0.63),
(1.00, 0.60),
],
0.50, 60.0, 0.8,
0.04,
),
FuelCellType::Mcfc => (
0.20,
vec![
(0.20, 0.47),
(0.40, 0.52),
(0.60, 0.55),
(0.80, 0.54),
(1.00, 0.51),
],
0.45, 120.0, 0.6,
0.05,
),
};
Self {
rated_power_mw: rated_mw,
fuel_cell_type: fc_type,
min_power_fraction: min_frac,
efficiency_curve: curve,
heat_recovery_fraction: heat_rec,
startup_time_min: startup,
degradation_pct_per_kh: deg_rate,
operating_hours: 0.0,
parasitic_fraction: parasitic,
}
}
pub fn efficiency_at(&self, power_fraction: f64) -> f64 {
let pf = power_fraction.clamp(0.0, 1.0);
let nominal_eff = interpolate_curve(&self.efficiency_curve, pf);
let deg_frac =
(self.degradation_pct_per_kh * self.operating_hours / 1000.0 / 100.0).clamp(0.0, 0.4);
nominal_eff * (1.0 - deg_frac)
}
pub fn hydrogen_consumption_kg_per_h(&self, power_mw: f64) -> f64 {
if self.rated_power_mw < 1e-12 || power_mw <= 0.0 {
return 0.0;
}
let power_fraction = (power_mw / self.rated_power_mw).clamp(0.0, 1.0);
if power_fraction < self.min_power_fraction - 1e-9 {
return 0.0;
}
let eff = self.efficiency_at(power_fraction);
if eff < 1e-12 {
return 0.0;
}
let power_kw = power_mw * 1000.0;
power_kw / (eff * H2_HHV_KWH_PER_KG)
}
pub fn heat_output_mw(&self, power_mw: f64) -> f64 {
if power_mw <= 0.0 || self.rated_power_mw < 1e-12 {
return 0.0;
}
let power_fraction = (power_mw / self.rated_power_mw).clamp(0.0, 1.0);
let eff = self.efficiency_at(power_fraction);
if !(1e-12..1.0).contains(&eff) {
return 0.0;
}
let heat_rejected_mw = power_mw * (1.0 - eff) / eff;
heat_rejected_mw * self.heat_recovery_fraction
}
pub fn net_power_mw(&self, power_mw: f64) -> f64 {
power_mw * (1.0 - self.parasitic_fraction)
}
pub fn water_production_kg_per_h(&self, power_mw: f64) -> f64 {
let h2_consumed = self.hydrogen_consumption_kg_per_h(power_mw);
h2_consumed * H2O_PER_H2_KG
}
pub fn max_h2_consumption_kg_per_h(&self) -> f64 {
self.hydrogen_consumption_kg_per_h(self.rated_power_mw)
}
pub fn total_chp_efficiency(&self, power_fraction: f64) -> f64 {
let eff_e = self.efficiency_at(power_fraction);
if eff_e < 1e-12 {
return 0.0;
}
let heat_fraction = (1.0 - eff_e) * self.heat_recovery_fraction;
(eff_e + heat_fraction).clamp(0.0, 1.0)
}
}
fn interpolate_curve(curve: &[(f64, f64)], x: f64) -> f64 {
if curve.is_empty() {
return 0.0;
}
if curve.len() == 1 {
return curve[0].1;
}
if x <= curve[0].0 {
return curve[0].1;
}
if x >= curve[curve.len() - 1].0 {
return curve[curve.len() - 1].1;
}
for i in 1..curve.len() {
let (x0, y0) = curve[i - 1];
let (x1, y1) = curve[i];
if x <= x1 {
let t = if (x1 - x0).abs() < 1e-15 {
0.0
} else {
(x - x0) / (x1 - x0)
};
return y0 + t * (y1 - y0);
}
}
curve[curve.len() - 1].1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fuel_cell_h2_consumption() {
let fc = FuelCell::new(1.0, FuelCellType::Pemfc);
let h2 = fc.hydrogen_consumption_kg_per_h(1.0);
assert!(h2 > 30.0, "Expected H2 consumption > 30 kg/h, got {h2:.3}");
assert!(h2 < 80.0, "Expected H2 consumption < 80 kg/h, got {h2:.3}");
}
#[test]
fn test_fuel_cell_heat_output() {
let fc = FuelCell::new(2.0, FuelCellType::Sofc);
let heat = fc.heat_output_mw(2.0);
assert!(
heat > 0.0,
"SOFC heat output should be positive, got {heat:.4} MW"
);
}
#[test]
fn test_fuel_cell_heat_scales_with_power() {
let fc = FuelCell::new(5.0, FuelCellType::Pemfc);
let heat_full = fc.heat_output_mw(5.0);
let heat_half = fc.heat_output_mw(2.5);
assert!(
heat_full > heat_half,
"Full-power heat should exceed half-power heat"
);
}
#[test]
fn test_fuel_cell_zero_output_at_zero_power() {
let fc = FuelCell::new(1.0, FuelCellType::Pemfc);
assert_eq!(fc.hydrogen_consumption_kg_per_h(0.0), 0.0);
assert_eq!(fc.heat_output_mw(0.0), 0.0);
assert_eq!(fc.water_production_kg_per_h(0.0), 0.0);
}
#[test]
fn test_fuel_cell_water_production_stoichiometry() {
let fc = FuelCell::new(1.0, FuelCellType::Pemfc);
let h2 = fc.hydrogen_consumption_kg_per_h(1.0);
let water = fc.water_production_kg_per_h(1.0);
let ratio = water / h2;
assert!(
(ratio - 9.0).abs() < 0.01,
"Water/H2 ratio should be 9.0, got {ratio:.4}"
);
}
#[test]
fn test_fuel_cell_chp_efficiency_exceeds_electrical() {
let fc = FuelCell::new(1.0, FuelCellType::Sofc);
let eff_e = fc.efficiency_at(0.6);
let eff_chp = fc.total_chp_efficiency(0.6);
assert!(
eff_chp > eff_e,
"CHP efficiency ({eff_chp:.4}) should exceed electrical ({eff_e:.4})"
);
}
#[test]
fn test_fuel_cell_below_min_load_returns_zero() {
let fc = FuelCell::new(1.0, FuelCellType::Mcfc);
let h2 = fc.hydrogen_consumption_kg_per_h(0.05);
assert_eq!(h2, 0.0, "Should not consume H2 below min load");
}
#[test]
fn test_fuel_cell_degradation() {
let mut fc = FuelCell::new(1.0, FuelCellType::Pemfc);
let eff_new = fc.efficiency_at(0.5);
fc.operating_hours = 20_000.0;
let eff_aged = fc.efficiency_at(0.5);
assert!(eff_aged < eff_new, "Aged FC should have lower efficiency");
}
}