#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HydrogenStorageType {
CompressedGas700bar,
CompressedGas350bar,
LiquidH2,
MetalHydride,
Underground,
}
#[derive(Debug, Clone)]
pub struct HydrogenTank {
pub capacity_kg: f64,
pub current_kg: f64,
pub storage_type: HydrogenStorageType,
pub max_charge_rate_kg_per_h: f64,
pub max_discharge_rate_kg_per_h: f64,
pub pressure_bar: f64,
pub max_pressure_bar: f64,
pub min_soc_fraction: f64,
pub boil_off_rate_pct_per_day: f64,
pub compression_energy_kwh_per_kg: f64,
}
impl HydrogenTank {
pub fn new(capacity_kg: f64, storage_type: HydrogenStorageType) -> Self {
let (max_p_bar, comp_energy, boil_off) = match storage_type {
HydrogenStorageType::CompressedGas700bar => (700.0, 2.5, 0.0),
HydrogenStorageType::CompressedGas350bar => (350.0, 1.5, 0.0),
HydrogenStorageType::LiquidH2 => (10.0, 9.5, 0.2), HydrogenStorageType::MetalHydride => (50.0, 0.5, 0.0),
HydrogenStorageType::Underground => (200.0, 0.8, 0.0),
};
let rate = capacity_kg / 8.0;
Self {
capacity_kg,
current_kg: 0.0,
storage_type,
max_charge_rate_kg_per_h: rate,
max_discharge_rate_kg_per_h: rate,
pressure_bar: 1.0, max_pressure_bar: max_p_bar,
min_soc_fraction: 0.05,
boil_off_rate_pct_per_day: boil_off,
compression_energy_kwh_per_kg: comp_energy,
}
}
pub fn soc(&self) -> f64 {
if self.capacity_kg < 1e-12 {
return 0.0;
}
(self.current_kg / self.capacity_kg).clamp(0.0, 1.0)
}
pub fn min_kg(&self) -> f64 {
self.min_soc_fraction * self.capacity_kg
}
pub fn available_headroom_kg(&self) -> f64 {
(self.capacity_kg - self.current_kg).max(0.0)
}
pub fn available_to_discharge_kg(&self) -> f64 {
(self.current_kg - self.min_kg()).max(0.0)
}
pub fn charge(&mut self, h2_kg: f64, dt_hours: f64) -> f64 {
if h2_kg <= 0.0 || dt_hours <= 0.0 {
return 0.0;
}
let rate_limited = (h2_kg / dt_hours).min(self.max_charge_rate_kg_per_h) * dt_hours;
let room = self.available_headroom_kg();
let actual = rate_limited.min(room).max(0.0);
self.current_kg += actual;
self.current_kg = self.current_kg.clamp(0.0, self.capacity_kg);
self.pressure_bar = self.pressure_from_fill();
actual
}
pub fn discharge(&mut self, h2_kg: f64, dt_hours: f64) -> f64 {
if h2_kg <= 0.0 || dt_hours <= 0.0 {
return 0.0;
}
let rate_limited = (h2_kg / dt_hours).min(self.max_discharge_rate_kg_per_h) * dt_hours;
let available = self.available_to_discharge_kg();
let actual = rate_limited.min(available).max(0.0);
self.current_kg -= actual;
self.current_kg = self.current_kg.clamp(self.min_kg(), self.capacity_kg);
self.pressure_bar = self.pressure_from_fill();
actual
}
pub fn boil_off(&mut self, dt_hours: f64) -> f64 {
if self.boil_off_rate_pct_per_day < 1e-12 || self.current_kg < 1e-12 {
return 0.0;
}
let hourly_rate_frac = self.boil_off_rate_pct_per_day / 100.0 / 24.0;
let lost = self.current_kg * hourly_rate_frac * dt_hours;
let lost = lost.min(self.current_kg - self.min_kg()).max(0.0);
self.current_kg -= lost;
self.current_kg = self.current_kg.max(0.0);
self.pressure_bar = self.pressure_from_fill();
lost
}
pub fn pressure_from_fill(&self) -> f64 {
match self.storage_type {
HydrogenStorageType::CompressedGas700bar | HydrogenStorageType::CompressedGas350bar => {
(self.max_pressure_bar * self.soc()).max(1.0)
}
_ => self.max_pressure_bar,
}
}
pub fn compression_energy_kwh(&self, h2_to_store_kg: f64) -> f64 {
h2_to_store_kg.max(0.0) * self.compression_energy_kwh_per_kg
}
pub fn is_full(&self) -> bool {
self.current_kg >= self.capacity_kg * 0.95
}
pub fn is_empty(&self) -> bool {
self.current_kg <= self.min_kg() + 1e-9
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tank(capacity_kg: f64) -> HydrogenTank {
HydrogenTank::new(capacity_kg, HydrogenStorageType::CompressedGas700bar)
}
#[test]
fn test_tank_initial_state() {
let tank = make_tank(1000.0);
assert_eq!(tank.current_kg, 0.0);
assert!((tank.soc()).abs() < 1e-9);
}
#[test]
fn test_tank_charge_discharge() {
let mut tank = make_tank(1000.0);
let charged = tank.charge(100.0, 1.0);
assert!(
(charged - 100.0).abs() < 1.0,
"Should charge ~100 kg, got {charged}"
);
assert!(tank.soc() > 0.0);
let discharged = tank.discharge(50.0, 1.0);
assert!(
(discharged - 50.0).abs() < 1.0,
"Should discharge ~50 kg, got {discharged}"
);
}
#[test]
fn test_tank_min_soc_respected() {
let mut tank = make_tank(1000.0);
tank.current_kg = 50.0; let discharged = tank.discharge(100.0, 1.0);
assert!(
discharged < 1e-9,
"Should not discharge below min SoC, discharged {discharged:.6} kg"
);
}
#[test]
fn test_tank_capacity_limit() {
let mut tank = make_tank(100.0);
let charged = tank.charge(200.0, 1.0);
assert!(tank.current_kg <= 100.0);
assert!(charged <= 100.0);
}
#[test]
fn test_tank_rate_limit() {
let mut tank = make_tank(10_000.0);
let charged = tank.charge(2000.0, 1.0);
assert!(
charged <= tank.max_charge_rate_kg_per_h + 1.0,
"Should be rate-limited, charged {charged:.2} kg"
);
}
#[test]
fn test_tank_boil_off_liquid_h2() {
let mut tank = HydrogenTank::new(5000.0, HydrogenStorageType::LiquidH2);
tank.current_kg = 1000.0;
let initial = tank.current_kg;
let lost = tank.boil_off(24.0); assert!(lost > 0.0, "Liquid H2 should have boil-off");
assert!(
tank.current_kg < initial,
"Tank level should decrease due to boil-off"
);
}
#[test]
fn test_tank_no_boil_off_compressed() {
let mut tank = make_tank(1000.0);
tank.current_kg = 500.0;
let lost = tank.boil_off(24.0);
assert_eq!(lost, 0.0, "Compressed gas has no boil-off");
}
#[test]
fn test_tank_pressure_from_fill() {
let mut tank = make_tank(1000.0);
tank.current_kg = 500.0; let p = tank.pressure_from_fill();
assert!(
(p - 350.0).abs() < 1.0,
"Pressure should be ~350 bar at 50% fill, got {p:.2}"
);
}
#[test]
fn test_compression_energy() {
let tank = make_tank(1000.0);
let energy = tank.compression_energy_kwh(10.0);
assert!(
(energy - 25.0).abs() < 0.1,
"Expected 25 kWh compression energy, got {energy:.3}"
);
}
#[test]
fn test_soc_clamped() {
let mut tank = make_tank(100.0);
tank.current_kg = 150.0; assert_eq!(tank.soc(), 1.0); }
}