use serde::{Deserialize, Serialize};
use crate::amino_catabolism::{AminoCatabConfig, AminoCatabFlux, AminoCatabState};
use crate::beta_oxidation::{BetaOxConfig, BetaOxFlux, BetaOxState};
use crate::error::RasayanError;
use crate::etc::{EtcConfig, EtcFlux, EtcState};
use crate::glycolysis::{GlycolysisConfig, GlycolysisFlux, GlycolysisState};
use crate::tca::{TcaConfig, TcaFlux, TcaState};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MetabolicNetwork {
pub glycolysis: GlycolysisState,
pub tca: TcaState,
pub etc: EtcState,
pub beta_ox: BetaOxState,
pub amino_catab: AminoCatabState,
pub atp: f64,
pub adp: f64,
pub nad_ratio: f64,
pub oxygen: f64,
pub malonyl_coa: f64,
}
impl Default for MetabolicNetwork {
fn default() -> Self {
Self {
glycolysis: GlycolysisState::default(),
tca: TcaState::default(),
etc: EtcState::default(),
beta_ox: BetaOxState::default(),
amino_catab: AminoCatabState::default(),
atp: 6.0,
adp: 0.5,
nad_ratio: 700.0,
oxygen: 1.0,
malonyl_coa: 0.01,
}
}
}
impl MetabolicNetwork {
#[must_use = "validation errors should be handled"]
pub fn validate(&self) -> Result<(), RasayanError> {
self.glycolysis.validate()?;
self.tca.validate()?;
self.etc.validate()?;
self.beta_ox.validate()?;
self.amino_catab.validate()?;
if self.atp < 0.0 {
return Err(RasayanError::NegativeConcentration {
name: "atp".into(),
value: self.atp,
});
}
if self.adp < 0.0 {
return Err(RasayanError::NegativeConcentration {
name: "adp".into(),
value: self.adp,
});
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NetworkConfig {
pub glycolysis: GlycolysisConfig,
pub tca: TcaConfig,
pub etc: EtcConfig,
pub beta_ox: BetaOxConfig,
pub amino_catab: AminoCatabConfig,
pub nad_ratio_sensitivity: f64,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
glycolysis: GlycolysisConfig::default(),
tca: TcaConfig::default(),
etc: EtcConfig::default(),
beta_ox: BetaOxConfig::default(),
amino_catab: AminoCatabConfig::default(),
nad_ratio_sensitivity: 50.0,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[must_use]
pub struct NetworkFlux {
pub glycolysis: GlycolysisFlux,
pub tca: TcaFlux,
pub etc: EtcFlux,
pub beta_ox: BetaOxFlux,
pub amino_catab: AminoCatabFlux,
pub net_atp: f64,
pub o2_consumed: f64,
pub co2_produced: f64,
}
impl MetabolicNetwork {
#[must_use = "network flux contains metabolic accounting"]
pub fn tick(&mut self, config: &NetworkConfig, dt: f64) -> NetworkFlux {
tracing::trace!(dt, atp = self.atp, adp = self.adp, "network_tick");
let gflux =
self.glycolysis
.tick(&config.glycolysis, self.atp, self.adp, self.nad_ratio, dt);
let bflux = self
.beta_ox
.tick(&config.beta_ox, self.malonyl_coa, self.nad_ratio, dt);
let aflux =
self.amino_catab
.tick(&config.amino_catab, self.tca.alpha_kg, self.nad_ratio, dt);
self.tca.acetyl_coa += bflux.acetyl_coa_produced + aflux.to_acetyl_coa;
self.tca.alpha_kg += aflux.to_alpha_kg;
self.tca.succinyl_coa += aflux.to_succinyl_coa;
self.tca.fumarate += aflux.to_fumarate;
self.tca.oxaloacetate += aflux.to_oxaloacetate;
let pyruvate_input = self.glycolysis.pyruvate + aflux.to_pyruvate;
let tflux = self.tca.tick(
&config.tca,
pyruvate_input,
self.atp,
self.adp,
self.nad_ratio,
dt,
);
self.glycolysis.pyruvate = (self.glycolysis.pyruvate - tflux.pyruvate_consumed).max(0.0);
let total_nadh =
gflux.nadh_produced + tflux.nadh_produced + bflux.nadh_produced + aflux.nadh_produced;
let total_fadh2 = tflux.fadh2_produced + bflux.fadh2_produced;
let eflux = self.etc.tick(
&config.etc,
total_nadh,
total_fadh2,
self.oxygen,
self.adp,
dt,
);
let atp_produced = gflux.atp_produced + tflux.gtp_produced + eflux.atp_produced;
let atp_consumed = gflux.atp_consumed + bflux.atp_consumed;
let net = atp_produced - atp_consumed;
self.atp = (self.atp + net).max(0.0);
self.adp = (self.adp - net).max(0.0);
let nadh_net = total_nadh + total_fadh2 - eflux.nadh_consumed - eflux.fadh2_consumed;
if nadh_net.abs() > f64::EPSILON {
self.nad_ratio =
(self.nad_ratio - nadh_net * config.nad_ratio_sensitivity).clamp(1.0, 2000.0);
}
let net_atp = net;
let o2_consumed = eflux.o2_consumed;
let co2_produced = tflux.co2_produced;
NetworkFlux {
glycolysis: gflux,
tca: tflux,
etc: eflux,
beta_ox: bflux,
amino_catab: aflux,
net_atp,
o2_consumed,
co2_produced,
}
}
pub fn run_to_steady_state(
&mut self,
config: &NetworkConfig,
dt: f64,
tolerance: f64,
max_steps: usize,
) -> (NetworkFlux, usize) {
let alpha = 0.1; let mut ema_net_atp = f64::MAX;
let mut flux = self.tick(config, dt);
for step in 1..max_steps {
flux = self.tick(config, dt);
if ema_net_atp == f64::MAX {
ema_net_atp = flux.net_atp;
} else {
let prev = ema_net_atp;
ema_net_atp = alpha * flux.net_atp + (1.0 - alpha) * ema_net_atp;
if step > 10 && (ema_net_atp - prev).abs() < tolerance {
return (flux, step + 1);
}
}
}
(flux, max_steps)
}
#[must_use]
pub fn respiratory_quotient(&self, flux: &NetworkFlux) -> f64 {
if flux.o2_consumed > f64::EPSILON {
flux.co2_produced / flux.o2_consumed
} else {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_valid() {
assert!(MetabolicNetwork::default().validate().is_ok());
}
#[test]
fn test_single_tick() {
let mut net = MetabolicNetwork::default();
let config = NetworkConfig::default();
let flux = net.tick(&config, 0.1);
assert!(
flux.glycolysis.step_rates[0] >= 0.0,
"Glycolysis should be running"
);
}
#[test]
fn test_atp_maintained() {
let mut net = MetabolicNetwork::default();
let config = NetworkConfig::default();
for _ in 0..100 {
let _ = net.tick(&config, 0.1);
}
assert!(net.atp > 0.0, "ATP should be maintained: {}", net.atp);
}
#[test]
fn test_glucose_consumed() {
let mut net = MetabolicNetwork::default();
let config = NetworkConfig::default();
let initial_glucose = net.glycolysis.glucose;
for _ in 0..100 {
let _ = net.tick(&config, 0.1);
}
assert!(net.glycolysis.glucose < initial_glucose);
}
#[test]
fn test_o2_consumed() {
let mut net = MetabolicNetwork::default();
let config = NetworkConfig::default();
let mut total_o2 = 0.0;
for _ in 0..100 {
let flux = net.tick(&config, 0.1);
total_o2 += flux.o2_consumed;
}
assert!(total_o2 > 0.0, "O2 should be consumed");
}
#[test]
fn test_co2_produced() {
let mut net = MetabolicNetwork::default();
let config = NetworkConfig::default();
let mut total_co2 = 0.0;
for _ in 0..100 {
let flux = net.tick(&config, 0.1);
total_co2 += flux.co2_produced;
}
assert!(total_co2 > 0.0, "CO2 should be produced");
}
#[test]
fn test_steady_state_analysis() {
let mut net = MetabolicNetwork::default();
let config = NetworkConfig::default();
let (flux, steps) = net.run_to_steady_state(&config, 0.1, 1e-3, 5000);
assert!(
flux.net_atp.is_finite(),
"Flux should be finite after {steps} steps"
);
assert!(net.atp >= 0.0);
assert!(net.adp >= 0.0);
}
#[test]
fn test_respiratory_quotient_range() {
let mut net = MetabolicNetwork::default();
let config = NetworkConfig::default();
let mut flux = net.tick(&config, 0.1);
for _ in 0..50 {
flux = net.tick(&config, 0.1);
}
let rq = net.respiratory_quotient(&flux);
if flux.o2_consumed > 1e-10 {
assert!(
rq > 0.0 && rq < 2.0,
"RQ={rq}, expected 0.7-1.0 range for mixed fuel"
);
}
}
#[test]
fn test_no_oxygen_limits_etc() {
let mut net = MetabolicNetwork {
oxygen: 0.0,
..MetabolicNetwork::default()
};
let config = NetworkConfig::default();
for _ in 0..100 {
let _ = net.tick(&config, 0.1);
}
let flux = net.tick(&config, 0.1);
assert!(
flux.etc.complex_rates[3] < 1e-10,
"CIV should not run without O2"
);
}
#[test]
fn test_cofactor_pools_bounded() {
let mut net = MetabolicNetwork::default();
let config = NetworkConfig::default();
for _ in 0..1000 {
let _ = net.tick(&config, 0.01);
}
assert!(net.atp >= 0.0);
assert!(net.adp >= 0.0);
assert!(net.nad_ratio >= 1.0);
}
#[test]
fn test_serde_roundtrip() {
let net = MetabolicNetwork::default();
let json = serde_json::to_string(&net).unwrap();
let net2: MetabolicNetwork = serde_json::from_str(&json).unwrap();
assert_eq!(net, net2);
}
#[test]
fn test_serde_roundtrip_config() {
let config = NetworkConfig::default();
let json = serde_json::to_string(&config).unwrap();
let config2: NetworkConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, config2);
}
}