use serde::{Deserialize, Serialize};
use crate::error::RasayanError;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EtcState {
pub pmf: f64,
pub qh2_fraction: f64,
pub cytc_reduced: f64,
}
impl Default for EtcState {
fn default() -> Self {
Self {
pmf: 0.6,
qh2_fraction: 0.3,
cytc_reduced: 0.2,
}
}
}
impl EtcState {
#[must_use = "validation errors should be handled"]
pub fn validate(&self) -> Result<(), RasayanError> {
for (name, value) in [
("pmf", self.pmf),
("qh2_fraction", self.qh2_fraction),
("cytc_reduced", self.cytc_reduced),
] {
if !(0.0..=1.0).contains(&value) {
return Err(RasayanError::InvalidParameter {
name: name.into(),
value,
reason: "must be in range 0.0-1.0".into(),
});
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EtcConfig {
pub complex_i_vmax: f64,
pub complex_ii_vmax: f64,
pub complex_iii_vmax: f64,
pub complex_iv_vmax: f64,
pub complex_iv_km_o2: f64,
pub atp_synthase_vmax: f64,
pub atp_synthase_pmf_threshold: f64,
pub atp_synthase_km_adp: f64,
pub proton_leak_rate: f64,
pub proton_leak_h_per_unit: f64,
pub pmf_capacity: f64,
pub h_per_nadh: f64,
pub h_per_fadh2: f64,
pub h_per_atp: f64,
}
impl Default for EtcConfig {
fn default() -> Self {
Self {
complex_i_vmax: 0.5,
complex_ii_vmax: 0.2,
complex_iii_vmax: 0.8,
complex_iv_vmax: 0.6,
complex_iv_km_o2: 0.01,
atp_synthase_vmax: 1.0,
atp_synthase_pmf_threshold: 0.3,
atp_synthase_km_adp: 0.1,
proton_leak_rate: 0.05,
proton_leak_h_per_unit: 10.0,
pmf_capacity: 100.0,
h_per_nadh: 10.0,
h_per_fadh2: 6.0,
h_per_atp: 4.0,
}
}
}
impl EtcConfig {
#[must_use = "validation errors should be handled"]
pub fn validate(&self) -> Result<(), RasayanError> {
for (name, value) in [
("complex_i_vmax", self.complex_i_vmax),
("complex_ii_vmax", self.complex_ii_vmax),
("complex_iii_vmax", self.complex_iii_vmax),
("complex_iv_vmax", self.complex_iv_vmax),
("complex_iv_km_o2", self.complex_iv_km_o2),
("atp_synthase_vmax", self.atp_synthase_vmax),
(
"atp_synthase_pmf_threshold",
self.atp_synthase_pmf_threshold,
),
("atp_synthase_km_adp", self.atp_synthase_km_adp),
("proton_leak_rate", self.proton_leak_rate),
("proton_leak_h_per_unit", self.proton_leak_h_per_unit),
("pmf_capacity", self.pmf_capacity),
("h_per_nadh", self.h_per_nadh),
("h_per_fadh2", self.h_per_fadh2),
("h_per_atp", self.h_per_atp),
] {
if value < 0.0 {
return Err(RasayanError::InvalidParameter {
name: name.into(),
value,
reason: "must be non-negative".into(),
});
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[must_use]
pub struct EtcFlux {
pub nadh_consumed: f64,
pub fadh2_consumed: f64,
pub o2_consumed: f64,
pub atp_produced: f64,
pub h2o_produced: f64,
pub complex_rates: [f64; 5],
}
impl EtcState {
#[must_use = "flux contains ATP/O2 accounting that should be applied"]
pub fn tick(
&mut self,
config: &EtcConfig,
nadh: f64,
fadh2: f64,
oxygen: f64,
adp: f64,
dt: f64,
) -> EtcFlux {
tracing::trace!(dt, nadh, fadh2, oxygen, pmf = self.pmf, "etc_tick");
let pumping_capacity = (1.0 - self.pmf).max(0.0);
let ci_h = 4.0;
let ciii_h = 4.0;
let civ_h = 2.0;
let q_available = 1.0 - self.qh2_fraction;
let v_ci = config.complex_i_vmax * nadh.min(1.0) * q_available * pumping_capacity;
let v_cii = config.complex_ii_vmax * fadh2.min(1.0) * q_available;
let cytc_oxidized = 1.0 - self.cytc_reduced;
let v_ciii = config.complex_iii_vmax * self.qh2_fraction * cytc_oxidized * pumping_capacity;
let o2_factor = oxygen.clamp(0.0, 1.0) / (config.complex_iv_km_o2 + oxygen.clamp(0.0, 1.0));
let v_civ = config.complex_iv_vmax * self.cytc_reduced * o2_factor * pumping_capacity;
let pmf_drive = if self.pmf > config.atp_synthase_pmf_threshold {
(self.pmf - config.atp_synthase_pmf_threshold)
/ (1.0 - config.atp_synthase_pmf_threshold)
} else {
0.0
};
let adp_factor = adp / (config.atp_synthase_km_adp + adp);
let v_atp = config.atp_synthase_vmax * pmf_drive * adp_factor;
let v_leak = config.proton_leak_rate * self.pmf;
self.qh2_fraction += (v_ci + v_cii - v_ciii) * dt;
self.qh2_fraction = self.qh2_fraction.clamp(0.0, 1.0);
self.cytc_reduced += (v_ciii - v_civ) * dt;
self.cytc_reduced = self.cytc_reduced.clamp(0.0, 1.0);
let h_pumped = v_ci * ci_h + v_ciii * ciii_h + v_civ * civ_h;
let h_consumed = v_atp * config.h_per_atp + v_leak * config.proton_leak_h_per_unit;
self.pmf += (h_pumped - h_consumed) / config.pmf_capacity * dt;
self.pmf = self.pmf.clamp(0.0, 1.0);
let nadh_consumed = v_ci * dt;
let fadh2_consumed = v_cii * dt;
let o2_consumed = v_civ * 0.5 * dt;
let atp_produced = v_atp * dt;
let h2o_produced = o2_consumed;
EtcFlux {
nadh_consumed,
fadh2_consumed,
o2_consumed,
atp_produced,
h2o_produced,
complex_rates: [v_ci, v_cii, v_ciii, v_civ, v_atp],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run_etc(steps: usize, dt: f64) -> (EtcState, EtcFlux) {
let mut state = EtcState::default();
let config = EtcConfig::default();
let mut total = EtcFlux {
nadh_consumed: 0.0,
fadh2_consumed: 0.0,
o2_consumed: 0.0,
atp_produced: 0.0,
h2o_produced: 0.0,
complex_rates: [0.0; 5],
};
for _ in 0..steps {
let flux = state.tick(&config, 0.5, 0.1, 1.0, 0.5, dt);
total.nadh_consumed += flux.nadh_consumed;
total.fadh2_consumed += flux.fadh2_consumed;
total.o2_consumed += flux.o2_consumed;
total.atp_produced += flux.atp_produced;
total.h2o_produced += flux.h2o_produced;
total.complex_rates = flux.complex_rates;
}
(state, total)
}
#[test]
fn test_default_state_valid() {
assert!(EtcState::default().validate().is_ok());
}
#[test]
fn test_default_config_valid() {
assert!(EtcConfig::default().validate().is_ok());
}
#[test]
fn test_single_tick_rates_non_negative() {
let mut state = EtcState::default();
let config = EtcConfig::default();
let flux = state.tick(&config, 0.5, 0.1, 1.0, 0.5, 0.01);
for (i, &rate) in flux.complex_rates.iter().enumerate() {
assert!(rate >= 0.0, "Complex {} rate is negative: {}", i, rate);
}
}
#[test]
fn test_atp_produced() {
let (_, flux) = run_etc(100, 0.1);
assert!(flux.atp_produced > 0.0, "ATP should be produced");
}
#[test]
fn test_nadh_consumed() {
let (_, flux) = run_etc(100, 0.1);
assert!(flux.nadh_consumed > 0.0, "NADH should be consumed");
}
#[test]
fn test_o2_consumed() {
let (_, flux) = run_etc(100, 0.1);
assert!(flux.o2_consumed > 0.0, "O2 should be consumed");
}
#[test]
fn test_no_oxygen_no_civ() {
let mut state = EtcState::default();
let config = EtcConfig::default();
let flux = state.tick(&config, 0.5, 0.1, 0.0, 0.5, 0.1);
assert!(
flux.complex_rates[3] < 1e-10,
"CIV should not run without O2"
);
}
#[test]
fn test_no_nadh_no_ci() {
let mut state = EtcState::default();
let config = EtcConfig::default();
let flux = state.tick(&config, 0.0, 0.0, 1.0, 0.5, 0.1);
assert!(
flux.complex_rates[0] < 1e-10,
"CI should not run without NADH"
);
}
#[test]
fn test_respiratory_control() {
let config = EtcConfig::default();
let mut state_low = EtcState {
pmf: 0.2,
..EtcState::default()
};
let flux_low = state_low.tick(&config, 0.5, 0.1, 1.0, 0.5, 0.1);
let mut state_high = EtcState {
pmf: 0.9,
..EtcState::default()
};
let flux_high = state_high.tick(&config, 0.5, 0.1, 1.0, 0.5, 0.1);
assert!(
flux_high.complex_rates[0] < flux_low.complex_rates[0],
"CI should be slower at high pmf"
);
}
#[test]
fn test_adp_drives_atp_synthase() {
let config = EtcConfig::default();
let mut state1 = EtcState::default();
let flux_low_adp = state1.tick(&config, 0.5, 0.1, 1.0, 0.01, 0.1);
let mut state2 = EtcState::default();
let flux_high_adp = state2.tick(&config, 0.5, 0.1, 1.0, 2.0, 0.1);
assert!(
flux_high_adp.complex_rates[4] > flux_low_adp.complex_rates[4],
"ATP synthase should be faster with more ADP"
);
}
#[test]
fn test_pmf_stays_bounded() {
let (state, _) = run_etc(1000, 0.01);
assert!((0.0..=1.0).contains(&state.pmf));
assert!((0.0..=1.0).contains(&state.qh2_fraction));
assert!((0.0..=1.0).contains(&state.cytc_reduced));
}
#[test]
fn test_atp_per_nadh_ratio() {
let mut state = EtcState::default();
let config = EtcConfig::default();
let mut total_nadh = 0.0;
let mut total_atp = 0.0;
for _ in 0..500 {
let flux = state.tick(&config, 1.0, 0.0, 1.0, 1.0, 0.1);
total_nadh += flux.nadh_consumed;
total_atp += flux.atp_produced;
}
if total_nadh > 0.01 {
let ratio = total_atp / total_nadh;
assert!(
ratio > 1.0 && ratio < 4.0,
"ATP/NADH ratio = {ratio:.2}, expected ~2.5"
);
}
}
#[test]
fn test_serde_roundtrip_state() {
let state = EtcState::default();
let json = serde_json::to_string(&state).unwrap();
let state2: EtcState = serde_json::from_str(&json).unwrap();
assert_eq!(state, state2);
}
#[test]
fn test_serde_roundtrip_config() {
let config = EtcConfig::default();
let json = serde_json::to_string(&config).unwrap();
let config2: EtcConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, config2);
}
#[test]
fn test_h2o_equals_half_o2() {
let (_, flux) = run_etc(100, 0.1);
assert!(
(flux.h2o_produced - flux.o2_consumed).abs() < 1e-10,
"H2O should equal O2 consumed (half-reaction stoichiometry)"
);
}
}