use serde::{Deserialize, Serialize};
use crate::calcium::{CalciumConfig, CalciumFlux, CalciumState};
use crate::error::RasayanError;
use crate::jak_stat::{JakStatConfig, JakStatFlux, JakStatState};
use crate::mapk::{MapkConfig, MapkFlux, MapkState};
use crate::pi3k::{Pi3kConfig, Pi3kFlux, Pi3kState};
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SignalingNetwork {
pub mapk: MapkState,
pub pi3k: Pi3kState,
pub jak_stat: JakStatState,
pub calcium: CalciumState,
}
impl SignalingNetwork {
#[must_use = "validation errors should be handled"]
pub fn validate(&self) -> Result<(), RasayanError> {
self.mapk.validate()?;
self.pi3k.validate()?;
self.jak_stat.validate()?;
self.calcium.validate()?;
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SignalingConfig {
pub mapk: MapkConfig,
pub pi3k: Pi3kConfig,
pub jak_stat: JakStatConfig,
pub calcium: CalciumConfig,
pub ras_pi3k_crosstalk: f64,
pub akt_raf_inhibition: f64,
pub ca_ras_crosstalk: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[must_use]
pub struct SignalingFlux {
pub mapk: MapkFlux,
pub pi3k: Pi3kFlux,
pub jak_stat: JakStatFlux,
pub calcium: CalciumFlux,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SignalingInput {
pub growth_factor: f64,
pub cytokine: f64,
pub ip3: f64,
}
impl Default for SignalingInput {
fn default() -> Self {
Self {
growth_factor: 0.0,
cytokine: 0.0,
ip3: 0.0,
}
}
}
impl SignalingNetwork {
#[must_use = "flux contains signaling pathway outputs"]
pub fn tick(
&mut self,
config: &SignalingConfig,
input: &SignalingInput,
dt: f64,
) -> SignalingFlux {
tracing::trace!(dt, gf = input.growth_factor, "signaling_tick");
let pi3k_input = input.growth_factor + self.mapk.ras_gtp * config.ras_pi3k_crosstalk;
let ca_boost = (self.calcium.cytoplasmic_ca / 1.0).min(1.0) * config.ca_ras_crosstalk;
let mapk_input = (input.growth_factor + ca_boost).min(1.0);
let mapk_flux = self.mapk.tick(&config.mapk, mapk_input, dt);
let pi3k_flux = self.pi3k.tick(&config.pi3k, pi3k_input.min(1.0), dt);
let jak_stat_flux = self.jak_stat.tick(&config.jak_stat, input.cytokine, dt);
let ca_flux = self.calcium.tick(&config.calcium, input.ip3, dt);
if config.akt_raf_inhibition > 0.0 {
let akt_inhibition = self.pi3k.akt_active * config.akt_raf_inhibition * dt;
self.mapk.raf_active = (self.mapk.raf_active - akt_inhibition).max(0.0);
}
SignalingFlux {
mapk: mapk_flux,
pi3k: pi3k_flux,
jak_stat: jak_stat_flux,
calcium: ca_flux,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_valid() {
assert!(SignalingNetwork::default().validate().is_ok());
}
#[test]
fn test_growth_factor_activates_mapk_and_pi3k() {
let mut net = SignalingNetwork::default();
let config = SignalingConfig::default();
let input = SignalingInput {
growth_factor: 1.0,
..SignalingInput::default()
};
for _ in 0..100 {
let _ = net.tick(&config, &input, 0.1);
}
assert!(net.mapk.erk_pp > MapkState::default().erk_pp);
assert!(net.pi3k.akt_active > Pi3kState::default().akt_active);
}
#[test]
fn test_cytokine_activates_jak_stat() {
let mut net = SignalingNetwork::default();
let config = SignalingConfig::default();
let input = SignalingInput {
cytokine: 1.0,
..SignalingInput::default()
};
for _ in 0..100 {
let _ = net.tick(&config, &input, 0.1);
}
assert!(net.jak_stat.stat_dimer > JakStatState::default().stat_dimer);
}
#[test]
fn test_ip3_raises_calcium() {
let mut net = SignalingNetwork::default();
let config = SignalingConfig::default();
let input = SignalingInput {
ip3: 1.0,
..SignalingInput::default()
};
for _ in 0..100 {
let _ = net.tick(&config, &input, 0.01);
}
assert!(net.calcium.cytoplasmic_ca > CalciumState::default().cytoplasmic_ca);
}
#[test]
fn test_ras_pi3k_crosstalk() {
let config_no_xt = SignalingConfig::default(); let config_xt = SignalingConfig {
ras_pi3k_crosstalk: 2.0,
..SignalingConfig::default()
};
let input = SignalingInput {
growth_factor: 0.3, ..SignalingInput::default()
};
let mut net1 = SignalingNetwork::default();
let mut net2 = SignalingNetwork::default();
for _ in 0..100 {
let _ = net1.tick(&config_no_xt, &input, 0.1);
let _ = net2.tick(&config_xt, &input, 0.1);
}
assert!(
net2.pi3k.akt_active > net1.pi3k.akt_active,
"Ras→PI3K crosstalk should boost Akt"
);
}
#[test]
fn test_bounded() {
let mut net = SignalingNetwork::default();
let config = SignalingConfig::default();
let input = SignalingInput {
growth_factor: 1.0,
cytokine: 0.5,
ip3: 0.5,
};
for _ in 0..1000 {
let _ = net.tick(&config, &input, 0.01);
}
assert!(net.validate().is_ok());
}
#[test]
fn test_serde_roundtrip() {
let net = SignalingNetwork::default();
let json = serde_json::to_string(&net).unwrap();
let net2: SignalingNetwork = serde_json::from_str(&json).unwrap();
assert_eq!(net, net2);
}
}