use serde::{Deserialize, Serialize};
use crate::enzyme;
use crate::error::RasayanError;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NuclearReceptorState {
pub bound_cytoplasmic: f64,
pub nuclear_active: f64,
pub gene_expression: f64,
}
impl Default for NuclearReceptorState {
fn default() -> Self {
Self {
bound_cytoplasmic: 0.05,
nuclear_active: 0.02,
gene_expression: 0.1,
}
}
}
impl NuclearReceptorState {
#[must_use = "validation errors should be handled"]
pub fn validate(&self) -> Result<(), RasayanError> {
for (name, value) in [
("bound_cytoplasmic", self.bound_cytoplasmic),
("nuclear_active", self.nuclear_active),
("gene_expression", self.gene_expression),
] {
if value < 0.0 {
return Err(RasayanError::NegativeConcentration {
name: name.into(),
value,
});
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NuclearReceptorConfig {
pub binding_vmax: f64,
pub binding_km: f64,
pub dissociation_rate: f64,
pub translocation_rate: f64,
pub export_rate: f64,
pub transcription_rate: f64,
pub expression_decay_rate: f64,
}
impl Default for NuclearReceptorConfig {
fn default() -> Self {
Self {
binding_vmax: 0.3,
binding_km: 0.2,
dissociation_rate: 0.1,
translocation_rate: 0.05,
export_rate: 0.02,
transcription_rate: 0.1,
expression_decay_rate: 0.01,
}
}
}
impl NuclearReceptorConfig {
#[must_use = "validation errors should be handled"]
pub fn validate(&self) -> Result<(), RasayanError> {
for (name, value) in [
("binding_vmax", self.binding_vmax),
("binding_km", self.binding_km),
("dissociation_rate", self.dissociation_rate),
("translocation_rate", self.translocation_rate),
("export_rate", self.export_rate),
("transcription_rate", self.transcription_rate),
("expression_decay_rate", self.expression_decay_rate),
] {
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 NuclearReceptorFlux {
pub binding: f64,
pub translocation: f64,
pub transcription: f64,
}
impl NuclearReceptorState {
#[must_use = "flux contains gene expression output"]
pub fn tick(
&mut self,
config: &NuclearReceptorConfig,
hormone: f64,
dt: f64,
) -> NuclearReceptorFlux {
tracing::trace!(
dt,
hormone,
nuclear = self.nuclear_active,
"nuclear_receptor_tick"
);
let v_bind = enzyme::michaelis_menten(
hormone.max(0.0),
config.binding_vmax * (1.0 - self.bound_cytoplasmic),
config.binding_km,
);
let v_dissoc = config.dissociation_rate * self.bound_cytoplasmic;
let v_translocate = config.translocation_rate * self.bound_cytoplasmic;
let v_export = config.export_rate * self.nuclear_active;
let v_transcribe = config.transcription_rate * self.nuclear_active;
let v_decay = config.expression_decay_rate * self.gene_expression;
self.bound_cytoplasmic += (v_bind - v_dissoc - v_translocate) * dt;
self.nuclear_active += (v_translocate - v_export) * dt;
self.gene_expression += (v_transcribe - v_decay) * dt;
self.bound_cytoplasmic = self.bound_cytoplasmic.clamp(0.0, 1.0);
self.nuclear_active = self.nuclear_active.clamp(0.0, 1.0);
self.gene_expression = self.gene_expression.max(0.0);
NuclearReceptorFlux {
binding: v_bind,
translocation: v_translocate,
transcription: v_transcribe,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_valid() {
assert!(NuclearReceptorState::default().validate().is_ok());
assert!(NuclearReceptorConfig::default().validate().is_ok());
}
#[test]
fn test_hormone_induces_expression() {
let mut state = NuclearReceptorState::default();
let config = NuclearReceptorConfig::default();
let initial = state.gene_expression;
for _ in 0..500 {
let _ = state.tick(&config, 1.0, 0.1);
}
assert!(
state.gene_expression > initial,
"Hormone should induce gene expression"
);
}
#[test]
fn test_delayed_response() {
let mut state = NuclearReceptorState::default();
let config = NuclearReceptorConfig::default();
let flux = state.tick(&config, 1.0, 0.1);
let early_transcription = flux.transcription;
for _ in 0..100 {
let _ = state.tick(&config, 1.0, 0.1);
}
let flux_later = state.tick(&config, 1.0, 0.1);
assert!(
flux_later.transcription > early_transcription,
"Response should build over time"
);
}
#[test]
fn test_no_hormone_decays() {
let mut state = NuclearReceptorState {
bound_cytoplasmic: 0.5,
nuclear_active: 0.5,
gene_expression: 1.0,
};
let config = NuclearReceptorConfig::default();
for _ in 0..500 {
let _ = state.tick(&config, 0.0, 0.1);
}
assert!(
state.nuclear_active < 0.3,
"Should decay without hormone: {}",
state.nuclear_active
);
}
#[test]
fn test_bounded() {
let mut state = NuclearReceptorState::default();
let config = NuclearReceptorConfig::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 = NuclearReceptorState::default();
let json = serde_json::to_string(&state).unwrap();
let state2: NuclearReceptorState = serde_json::from_str(&json).unwrap();
assert_eq!(state, state2);
}
}