use serde::{Deserialize, Serialize};
use crate::enzyme;
use crate::error::RasayanError;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MapkState {
pub ras_gtp: f64,
pub raf_active: f64,
pub mek_pp: f64,
pub erk_pp: f64,
}
impl Default for MapkState {
fn default() -> Self {
Self {
ras_gtp: 0.05,
raf_active: 0.02,
mek_pp: 0.01,
erk_pp: 0.01,
}
}
}
impl MapkState {
#[must_use = "validation errors should be handled"]
pub fn validate(&self) -> Result<(), RasayanError> {
for (name, value) in [
("ras_gtp", self.ras_gtp),
("raf_active", self.raf_active),
("mek_pp", self.mek_pp),
("erk_pp", self.erk_pp),
] {
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 MapkConfig {
pub ras_activation_rate: f64,
pub ras_gap_rate: f64,
pub raf_vmax: f64,
pub raf_km: f64,
pub raf_phosphatase_rate: f64,
pub mek_vmax: f64,
pub mek_km: f64,
pub mek_phosphatase_rate: f64,
pub erk_vmax: f64,
pub erk_km: f64,
pub erk_phosphatase_rate: f64,
pub erk_feedback_strength: f64,
}
impl Default for MapkConfig {
fn default() -> Self {
Self {
ras_activation_rate: 0.1,
ras_gap_rate: 1.0,
raf_vmax: 0.5,
raf_km: 0.1,
raf_phosphatase_rate: 0.4,
mek_vmax: 0.8,
mek_km: 0.1,
mek_phosphatase_rate: 0.5,
erk_vmax: 1.0,
erk_km: 0.1,
erk_phosphatase_rate: 0.5,
erk_feedback_strength: 0.5,
}
}
}
impl MapkConfig {
#[must_use = "validation errors should be handled"]
pub fn validate(&self) -> Result<(), RasayanError> {
for (name, value) in [
("ras_activation_rate", self.ras_activation_rate),
("ras_gap_rate", self.ras_gap_rate),
("raf_vmax", self.raf_vmax),
("raf_km", self.raf_km),
("raf_phosphatase_rate", self.raf_phosphatase_rate),
("mek_vmax", self.mek_vmax),
("mek_km", self.mek_km),
("mek_phosphatase_rate", self.mek_phosphatase_rate),
("erk_vmax", self.erk_vmax),
("erk_km", self.erk_km),
("erk_phosphatase_rate", self.erk_phosphatase_rate),
("erk_feedback_strength", self.erk_feedback_strength),
] {
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 MapkFlux {
pub ras_activation: f64,
pub raf_phosphorylation: f64,
pub mek_phosphorylation: f64,
pub erk_phosphorylation: f64,
}
impl MapkState {
#[must_use = "flux contains cascade activation rates"]
pub fn tick(&mut self, config: &MapkConfig, growth_factor: f64, dt: f64) -> MapkFlux {
tracing::trace!(dt, growth_factor, erk = self.erk_pp, "mapk_tick");
let gf = growth_factor.clamp(0.0, 1.0);
let feedback = 1.0 / (1.0 + self.erk_pp * config.erk_feedback_strength / 0.1);
let v_ras_on = config.ras_activation_rate * gf * (1.0 - self.ras_gtp) * feedback;
let v_ras_off = config.ras_gap_rate * self.ras_gtp;
self.ras_gtp += (v_ras_on - v_ras_off) * dt;
let v_raf_on = enzyme::michaelis_menten(
self.ras_gtp,
config.raf_vmax * (1.0 - self.raf_active),
config.raf_km,
) * feedback;
let v_raf_off = config.raf_phosphatase_rate * self.raf_active;
self.raf_active += (v_raf_on - v_raf_off) * dt;
let v_mek_on = enzyme::michaelis_menten(
self.raf_active,
config.mek_vmax * (1.0 - self.mek_pp),
config.mek_km,
);
let v_mek_off = config.mek_phosphatase_rate * self.mek_pp;
self.mek_pp += (v_mek_on - v_mek_off) * dt;
let v_erk_on = enzyme::michaelis_menten(
self.mek_pp,
config.erk_vmax * (1.0 - self.erk_pp),
config.erk_km,
);
let v_erk_off = config.erk_phosphatase_rate * self.erk_pp;
self.erk_pp += (v_erk_on - v_erk_off) * dt;
self.ras_gtp = self.ras_gtp.clamp(0.0, 1.0);
self.raf_active = self.raf_active.clamp(0.0, 1.0);
self.mek_pp = self.mek_pp.clamp(0.0, 1.0);
self.erk_pp = self.erk_pp.clamp(0.0, 1.0);
MapkFlux {
ras_activation: v_ras_on,
raf_phosphorylation: v_raf_on,
mek_phosphorylation: v_mek_on,
erk_phosphorylation: v_erk_on,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_valid() {
assert!(MapkState::default().validate().is_ok());
assert!(MapkConfig::default().validate().is_ok());
}
#[test]
fn test_growth_factor_activates_erk() {
let mut state = MapkState::default();
let config = MapkConfig::default();
for _ in 0..200 {
let _ = state.tick(&config, 1.0, 0.1);
}
assert!(
state.erk_pp > MapkState::default().erk_pp,
"ERK should activate"
);
}
#[test]
fn test_no_stimulus_low_activity() {
let mut state = MapkState::default();
let config = MapkConfig::default();
for _ in 0..200 {
let _ = state.tick(&config, 0.0, 0.1);
}
assert!(
state.erk_pp < 0.3,
"ERK should be low without stimulus: {}",
state.erk_pp
);
}
#[test]
fn test_negative_feedback() {
let config = MapkConfig::default();
let mut state = MapkState {
erk_pp: 0.9,
..MapkState::default()
};
let flux = state.tick(&config, 1.0, 0.1);
let mut state2 = MapkState {
erk_pp: 0.01,
..MapkState::default()
};
let flux2 = state2.tick(&config, 1.0, 0.1);
assert!(
flux.ras_activation < flux2.ras_activation,
"High ERK should suppress Ras activation"
);
}
#[test]
fn test_cascade_amplification() {
let mut state = MapkState::default();
let config = MapkConfig::default();
for _ in 0..500 {
let _ = state.tick(&config, 0.5, 0.1);
}
assert!(
state.erk_pp > state.ras_gtp * 0.5,
"Cascade should amplify signal"
);
}
#[test]
fn test_bounded() {
let mut state = MapkState::default();
let config = MapkConfig::default();
for _ in 0..1000 {
let _ = state.tick(&config, 1.0, 0.01);
}
assert!(state.validate().is_ok());
}
#[test]
fn test_serde_roundtrip() {
let state = MapkState::default();
let json = serde_json::to_string(&state).unwrap();
let state2: MapkState = serde_json::from_str(&json).unwrap();
assert_eq!(state, state2);
}
}