use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EnergyStorageError {
InvalidParameter,
OverVoltage,
UnderVoltage,
OverDischarge,
}
impl core::fmt::Display for EnergyStorageError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
EnergyStorageError::InvalidParameter => {
write!(f, "EnergyStorageError: invalid parameter")
}
EnergyStorageError::OverVoltage => {
write!(f, "EnergyStorageError: over-voltage on supercapacitor")
}
EnergyStorageError::UnderVoltage => {
write!(f, "EnergyStorageError: under-voltage on supercapacitor")
}
EnergyStorageError::OverDischarge => {
write!(f, "EnergyStorageError: battery over-discharged")
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Supercapacitor<S: ControlScalar> {
v: S,
c_sc: S,
r_sc: S,
v_max: S,
dt: S,
}
impl<S: ControlScalar> Supercapacitor<S> {
pub fn new(c_sc: S, r_sc: S, v_max: S, v_init: S, dt: S) -> Result<Self, EnergyStorageError> {
if c_sc <= S::ZERO || r_sc < S::ZERO || v_max <= S::ZERO || dt <= S::ZERO {
return Err(EnergyStorageError::InvalidParameter);
}
if v_init < S::ZERO || v_init > v_max {
return Err(EnergyStorageError::InvalidParameter);
}
Ok(Self {
v: v_init,
c_sc,
r_sc,
v_max,
dt,
})
}
pub fn step(&mut self, i_sc: S) -> Result<S, EnergyStorageError> {
let v_dot = if self.r_sc > S::EPSILON {
i_sc / self.c_sc - self.v / (self.r_sc * self.c_sc)
} else {
i_sc / self.c_sc
};
self.v += v_dot * self.dt;
if self.v < S::ZERO {
self.v = S::ZERO;
}
if self.v > self.v_max {
return Err(EnergyStorageError::OverVoltage);
}
let v_term = self.v - i_sc * self.r_sc;
Ok(v_term)
}
#[inline]
pub fn voltage(&self) -> S {
self.v
}
#[inline]
pub fn soc(&self) -> S {
(self.v / self.v_max).clamp_val(S::ZERO, S::ONE)
}
#[inline]
pub fn energy(&self) -> S {
S::HALF * self.c_sc * self.v * self.v
}
#[inline]
pub fn v_max(&self) -> S {
self.v_max
}
#[inline]
pub fn capacitance(&self) -> S {
self.c_sc
}
}
#[derive(Debug, Clone, Copy)]
struct SimpleBattery<S: ControlScalar> {
soc: S,
q_nom: S, r_bat: S, v_nom: S, dt: S,
}
impl<S: ControlScalar> SimpleBattery<S> {
fn new(q_nom: S, r_bat: S, v_nom: S, soc_init: S, dt: S) -> Result<Self, EnergyStorageError> {
if q_nom <= S::ZERO || r_bat < S::ZERO || v_nom <= S::ZERO || dt <= S::ZERO {
return Err(EnergyStorageError::InvalidParameter);
}
if soc_init < S::ZERO || soc_init > S::ONE {
return Err(EnergyStorageError::InvalidParameter);
}
Ok(Self {
soc: soc_init,
q_nom,
r_bat,
v_nom,
dt,
})
}
fn step(&mut self, current: S) -> S {
let soc_dot = -current / (S::from_f64(3600.0) * self.q_nom);
self.soc = (self.soc + soc_dot * self.dt).clamp_val(S::ZERO, S::ONE);
self.v_nom - self.r_bat * current
}
#[inline]
fn soc(&self) -> S {
self.soc
}
}
#[derive(Debug, Clone, Copy)]
pub struct HybridEnergyStorage<S: ControlScalar> {
battery: SimpleBattery<S>,
sc: Supercapacitor<S>,
alpha_base: S,
}
impl<S: ControlScalar> HybridEnergyStorage<S> {
#[allow(clippy::too_many_arguments)]
pub fn new(
q_nom: S,
r_bat: S,
v_nom: S,
bat_soc_init: S,
c_sc: S,
r_sc: S,
v_sc_max: S,
v_sc_init: S,
alpha_base: S,
dt: S,
) -> Result<Self, EnergyStorageError> {
if alpha_base < S::ZERO || alpha_base > S::ONE || dt <= S::ZERO {
return Err(EnergyStorageError::InvalidParameter);
}
let battery = SimpleBattery::new(q_nom, r_bat, v_nom, bat_soc_init, dt)?;
let sc = Supercapacitor::new(c_sc, r_sc, v_sc_max, v_sc_init, dt)?;
Ok(Self {
battery,
sc,
alpha_base,
})
}
pub fn default_hybrid() -> Result<Self, EnergyStorageError> {
Self::new(
S::from_f64(50.0),
S::from_f64(0.05),
S::from_f64(400.0),
S::from_f64(0.8),
S::from_f64(100.0),
S::from_f64(0.01),
S::from_f64(48.0),
S::from_f64(40.0),
S::from_f64(0.3),
S::from_f64(1e-3),
)
}
pub fn step(&mut self, i_total: S) -> Result<(S, S), EnergyStorageError> {
let (i_sc, i_bat) = self.split_current(i_total);
let v_bat = self.battery.step(i_bat);
let v_sc = self.sc.step(i_sc)?;
Ok((v_bat, v_sc))
}
pub fn split_current(&self, i_total: S) -> (S, S) {
let sc_soc = self.sc.soc();
let alpha = if sc_soc > S::from_f64(0.8) {
self.alpha_base * S::from_f64(1.5)
} else if sc_soc < S::from_f64(0.2) {
self.alpha_base * S::from_f64(0.5)
} else {
self.alpha_base
};
let alpha_clamped = alpha.clamp_val(S::ZERO, S::ONE);
let i_sc = i_total * alpha_clamped;
let i_bat = i_total * (S::ONE - alpha_clamped);
(i_sc, i_bat)
}
#[inline]
pub fn battery_soc(&self) -> S {
self.battery.soc()
}
#[inline]
pub fn sc_soc(&self) -> S {
self.sc.soc()
}
#[inline]
pub fn sc_energy(&self) -> S {
self.sc.energy()
}
#[inline]
pub fn sc_voltage(&self) -> S {
self.sc.voltage()
}
#[inline]
pub fn alpha_base(&self) -> S {
self.alpha_base
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sc_charges_and_discharges() {
let mut sc =
Supercapacitor::<f64>::new(1.0, 0.0, 20.0, 10.0, 0.1).expect("SC construction");
for _ in 0..5 {
sc.step(5.0).expect("charge step");
}
let v_after_charge = sc.voltage();
assert!(
v_after_charge > 10.0,
"Voltage should increase during charging, got {v_after_charge}"
);
let v_before_discharge = sc.voltage();
for _ in 0..5 {
sc.step(-5.0).expect("discharge step");
}
let v_after_discharge = sc.voltage();
assert!(
v_after_discharge < v_before_discharge,
"Voltage should decrease during discharging: {v_after_discharge} < {v_before_discharge}"
);
}
#[test]
fn sc_energy_correct() {
let sc = Supercapacitor::<f64>::new(1.0, 0.0, 20.0, 10.0, 0.1).expect("SC construction");
let energy = sc.energy();
assert!(
(energy - 50.0).abs() < 1e-10,
"Energy should be 50 J, got {energy}"
);
}
#[test]
fn sc_over_voltage_detected() {
let mut sc =
Supercapacitor::<f64>::new(1.0, 0.0, 12.0, 11.9, 0.1).expect("SC construction");
let result = sc.step(5.0);
assert!(
matches!(result, Err(EnergyStorageError::OverVoltage)),
"Expected OverVoltage, got {result:?}"
);
}
#[test]
fn sc_soc_in_range() {
let sc = Supercapacitor::<f64>::new(100.0, 0.01, 48.0, 40.0, 1e-3).expect("SC");
let soc = sc.soc();
assert!((0.0..=1.0).contains(&soc), "SoC {soc} must be in [0, 1]");
assert!(
(soc - 40.0 / 48.0).abs() < 1e-10,
"Expected SoC ≈ {:.4}, got {soc:.4}",
40.0 / 48.0
);
}
#[test]
fn hybrid_step_returns_two_voltages() {
let mut hess = HybridEnergyStorage::<f64>::default_hybrid().expect("default_hybrid");
let result = hess.step(10.0);
assert!(result.is_ok(), "Hybrid step should succeed, got {result:?}");
let (v_bat, v_sc) = result.unwrap();
assert!(
v_bat > 0.0,
"Battery voltage should be positive, got {v_bat}"
);
assert!(v_sc > 0.0, "SC voltage should be positive, got {v_sc}");
}
#[test]
fn split_ratio_shifts_with_sc_soc() {
let hess_high = HybridEnergyStorage::<f64>::new(
50.0, 0.05, 400.0, 0.8, 100.0, 0.01, 48.0,
48.0, 0.4, 1e-3,
)
.expect("high-SoC HESS");
let i_total = 100.0_f64;
let (i_sc_high, i_bat_high) = hess_high.split_current(i_total);
assert!(
(i_sc_high - 60.0).abs() < 1e-10,
"i_sc at high SoC should be 60, got {i_sc_high}"
);
assert!(
(i_bat_high - 40.0).abs() < 1e-10,
"i_bat at high SoC should be 40, got {i_bat_high}"
);
assert!(
(i_sc_high + i_bat_high - i_total).abs() < 1e-10,
"Currents must sum to i_total"
);
let hess_low = HybridEnergyStorage::<f64>::new(
50.0, 0.05, 400.0, 0.8, 100.0, 0.01, 48.0,
1.0, 0.4, 1e-3,
)
.expect("low-SoC HESS");
let (i_sc_low, i_bat_low) = hess_low.split_current(i_total);
assert!(
(i_sc_low - 20.0).abs() < 1e-10,
"i_sc at low SoC should be 20, got {i_sc_low}"
);
assert!(
(i_bat_low - 80.0).abs() < 1e-10,
"i_bat at low SoC should be 80, got {i_bat_low}"
);
assert!(
(i_sc_low + i_bat_low - i_total).abs() < 1e-10,
"Currents must sum to i_total"
);
}
#[test]
fn hybrid_battery_soc_decreases_on_discharge() {
let mut hess = HybridEnergyStorage::<f64>::default_hybrid().expect("default_hybrid");
let initial_bat_soc = hess.battery_soc();
for _ in 0..100 {
hess.step(50.0).expect("hybrid step");
}
assert!(
hess.battery_soc() < initial_bat_soc,
"Battery SoC should decrease during discharge: {} < {}",
hess.battery_soc(),
initial_bat_soc
);
}
#[test]
fn invalid_params_rejected() {
let res = HybridEnergyStorage::<f64>::new(
50.0, 0.05, 400.0, 0.8, 100.0, 0.01, 48.0, 40.0, 1.5, 1e-3,
);
assert!(
matches!(res, Err(EnergyStorageError::InvalidParameter)),
"Expected InvalidParameter for alpha_base > 1"
);
let res = HybridEnergyStorage::<f64>::new(
50.0, 0.05, 400.0, 0.8, 100.0, 0.01, 48.0, 40.0, 0.3, 0.0,
);
assert!(
matches!(res, Err(EnergyStorageError::InvalidParameter)),
"Expected InvalidParameter for dt = 0"
);
let res = Supercapacitor::<f64>::new(0.0, 0.01, 48.0, 40.0, 1e-3);
assert!(
matches!(res, Err(EnergyStorageError::InvalidParameter)),
"Expected InvalidParameter for c_sc = 0"
);
}
#[test]
fn sc_zero_current_stable() {
let mut sc = Supercapacitor::<f64>::new(10.0, 0.1, 50.0, 25.0, 1e-3).expect("SC");
let v_init = sc.voltage();
for _ in 0..100 {
sc.step(0.0).expect("zero current step");
}
let v_after = sc.voltage();
assert!(
v_after < v_init && v_after > v_init * 0.5,
"SC voltage should decay slowly with zero current: {v_after} vs init {v_init}"
);
}
}