use serde::{Deserialize, Serialize};
use crate::error::RasayanError;
pub const ATP_HYDROLYSIS_DG: f64 = -30.5;
pub const MET_O2_RATE: f64 = 3.5;
pub const PCR_DEPLETION_RATE: f64 = 0.02;
pub const PCR_RECOVERY_RATE: f64 = 0.05;
pub const PCR_DEMAND_THRESHOLD: f64 = 4.0;
pub const GLYCOGEN_DEPLETION_RATE: f64 = 0.005;
pub const GLYCOGEN_RECOVERY_RATE: f64 = 0.002;
pub const GLYCOGEN_RECOVERY_THRESHOLD: f64 = 1.5;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BioenergyState {
pub phosphocreatine: f64,
pub glycogen: f64,
pub met: f64,
pub anaerobic_threshold: f64,
}
impl Default for BioenergyState {
fn default() -> Self {
Self {
phosphocreatine: 0.9,
glycogen: 0.8,
met: 1.0,
anaerobic_threshold: 6.0,
}
}
}
impl BioenergyState {
#[must_use = "validation errors should be handled"]
pub fn validate(&self) -> Result<(), RasayanError> {
if self.phosphocreatine < 0.0 || self.phosphocreatine > 1.0 {
return Err(RasayanError::InvalidParameter {
name: "phosphocreatine".into(),
value: self.phosphocreatine,
reason: "must be in range 0.0-1.0".into(),
});
}
if self.glycogen < 0.0 || self.glycogen > 1.0 {
return Err(RasayanError::InvalidParameter {
name: "glycogen".into(),
value: self.glycogen,
reason: "must be in range 0.0-1.0".into(),
});
}
if self.met < 1.0 {
return Err(RasayanError::InvalidParameter {
name: "met".into(),
value: self.met,
reason: "must be >= 1.0".into(),
});
}
if self.anaerobic_threshold < 1.0 {
return Err(RasayanError::InvalidParameter {
name: "anaerobic_threshold".into(),
value: self.anaerobic_threshold,
reason: "must be >= 1.0".into(),
});
}
Ok(())
}
#[must_use]
#[inline]
pub fn is_anaerobic(&self) -> bool {
self.met > self.anaerobic_threshold
}
#[must_use]
#[inline]
pub fn o2_consumption(&self) -> f64 {
self.met * MET_O2_RATE
}
pub fn set_exertion(&mut self, met: f64) {
tracing::trace!(met, "set_exertion");
self.met = met.max(1.0);
}
pub fn tick(&mut self, dt_minutes: f64) {
tracing::trace!(dt_minutes, met = self.met, "bioenergy_tick");
let demand = (self.met - 1.0).max(0.0);
if demand > PCR_DEMAND_THRESHOLD {
self.phosphocreatine =
(self.phosphocreatine - demand * PCR_DEPLETION_RATE * dt_minutes).max(0.0);
} else {
self.phosphocreatine = (self.phosphocreatine + PCR_RECOVERY_RATE * dt_minutes).min(1.0);
}
self.glycogen = (self.glycogen - demand * GLYCOGEN_DEPLETION_RATE * dt_minutes).max(0.0);
if self.met <= GLYCOGEN_RECOVERY_THRESHOLD {
self.glycogen = (self.glycogen + GLYCOGEN_RECOVERY_RATE * dt_minutes).min(1.0);
}
}
#[must_use]
#[inline]
pub fn energy_available(&self) -> f64 {
(self.phosphocreatine * 0.3 + self.glycogen * 0.7).clamp(0.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resting_is_aerobic() {
let b = BioenergyState::default();
assert!(!b.is_anaerobic());
assert!((b.o2_consumption() - 3.5).abs() < 0.01);
}
#[test]
fn test_high_exertion_depletes() {
let mut b = BioenergyState::default();
b.set_exertion(8.0);
b.tick(10.0);
assert!(b.phosphocreatine < 0.9);
assert!(b.glycogen < 0.8);
}
#[test]
fn test_recovery_at_rest() {
let mut b = BioenergyState {
phosphocreatine: 0.3,
glycogen: 0.5,
..BioenergyState::default()
};
b.set_exertion(1.0);
b.tick(30.0);
assert!(b.phosphocreatine > 0.3);
assert!(b.glycogen > 0.5);
}
#[test]
fn test_serde_roundtrip() {
let b = BioenergyState::default();
let json = serde_json::to_string(&b).unwrap();
let b2: BioenergyState = serde_json::from_str(&json).unwrap();
assert_eq!(b, b2);
}
#[test]
fn test_validate_valid() {
assert!(BioenergyState::default().validate().is_ok());
}
#[test]
fn test_validate_negative_phosphocreatine() {
let b = BioenergyState {
phosphocreatine: -0.1,
..BioenergyState::default()
};
assert!(b.validate().is_err());
}
#[test]
fn test_energy_available_range() {
let b = BioenergyState::default();
let e = b.energy_available();
assert!((0.0..=1.0).contains(&e));
}
#[test]
fn test_complete_depletion() {
let mut b = BioenergyState::default();
b.set_exertion(10.0);
for _ in 0..100 {
b.tick(1.0);
}
assert!(b.phosphocreatine.abs() < f64::EPSILON);
assert!(b.glycogen.abs() < f64::EPSILON);
}
}