use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ChemicalSystemType {
MassAction,
EnzymeKinetics,
MetabolicNetwork,
ReactionDiffusion,
Catalytic,
}
#[derive(Debug, Clone)]
pub struct ChemicalConfig {
pub system_type: ChemicalSystemType,
pub dt: f64,
pub stiff_method: StiffIntegrationMethod,
pub jacobian_method: JacobianMethod,
pub concentration_tolerance: f64,
pub relative_tolerance: f64,
pub absolute_tolerance: f64,
pub enforce_positivity: bool,
pub enforce_conservation: bool,
}
impl Default for ChemicalConfig {
fn default() -> Self {
Self {
system_type: ChemicalSystemType::MassAction,
dt: 0.001,
stiff_method: StiffIntegrationMethod::BDF2,
jacobian_method: JacobianMethod::Analytical,
concentration_tolerance: 1e-12,
relative_tolerance: 1e-6,
absolute_tolerance: 1e-9,
enforce_positivity: true,
enforce_conservation: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StiffIntegrationMethod {
BDF2,
Rosenbrock,
ImplicitEuler,
CashKarp,
Adaptive,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum JacobianMethod {
Analytical,
FiniteDifference,
ComplexStep,
AutomaticDifferentiation,
}
#[derive(Debug, Clone)]
pub struct Reaction {
pub rate_constant: f64,
pub reactants: Vec<(usize, f64)>,
pub products: Vec<(usize, f64)>,
pub reaction_type: ReactionType,
pub arrhenius: Option<ArrheniusParams>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ReactionType {
Elementary,
MichaelisMenten,
Hill { coefficient: f64 },
CompetitiveInhibition,
NonCompetitiveInhibition,
ProductInhibition,
}
#[derive(Debug, Clone)]
pub struct ArrheniusParams {
pub pre_exponential: f64,
pub activation_energy: f64,
pub gas_constant: f64,
}
#[derive(Debug, Clone)]
pub struct ChemicalProperties {
pub num_species: usize,
pub species_names: Vec<String>,
pub initial_concentrations: Array1<f64>,
pub reactions: Vec<Reaction>,
pub temperature: f64,
pub volume: f64,
pub conservation_matrix: Option<Array2<f64>>,
}
#[derive(Debug, Clone)]
pub struct ChemicalState {
pub concentrations: Array1<f64>,
pub reaction_rates: Array1<f64>,
pub time: f64,
}
#[derive(Debug, Clone)]
pub struct ChemicalResult {
pub state: ChemicalState,
pub stats: ChemicalStats,
pub success: bool,
pub conservation_error: f64,
pub constraint_violation: f64,
}
#[derive(Debug, Clone)]
pub struct ChemicalStats {
pub function_evaluations: usize,
pub jacobian_evaluations: usize,
pub newton_iterations: usize,
pub converged: bool,
pub stiffness_ratio: f64,
pub reaction_rate_time: f64,
pub jacobian_time: f64,
}
pub struct ChemicalIntegrator {
config: ChemicalConfig,
properties: ChemicalProperties,
previous_state: Option<ChemicalState>,
#[allow(dead_code)]
jacobian_cache: Option<Array2<f64>>,
reaction_rate_history: Vec<Array1<f64>>,
}
impl ChemicalIntegrator {
pub fn new(config: ChemicalConfig, properties: ChemicalProperties) -> Self {
Self {
config,
properties,
previous_state: None,
jacobian_cache: None,
reaction_rate_history: Vec::new(),
}
}
pub fn step(&mut self, t: f64, state: &ChemicalState) -> IntegrateResult<ChemicalResult> {
let start_time = std::time::Instant::now();
let reaction_rates = self.calculate_reaction_rates(&state.concentrations)?;
let reaction_rate_time = start_time.elapsed().as_secs_f64();
let derivatives = self.calculate_concentration_derivatives(&reaction_rates)?;
let new_concentrations = match self.config.stiff_method {
StiffIntegrationMethod::BDF2 => {
self.bdf2_step(&state.concentrations, &derivatives, self.config.dt)?
}
StiffIntegrationMethod::ImplicitEuler => {
self.implicit_euler_step(&state.concentrations, &derivatives, self.config.dt)?
}
StiffIntegrationMethod::Rosenbrock => {
self.rosenbrock_step(&state.concentrations, &derivatives, self.config.dt)?
}
StiffIntegrationMethod::CashKarp => {
self.cash_karp_step(&state.concentrations, &derivatives, self.config.dt)?
}
StiffIntegrationMethod::Adaptive => {
self.adaptive_step(&state.concentrations, &derivatives, self.config.dt)?
}
};
let constrained_concentrations = if self.config.enforce_positivity {
self.enforce_positivity_constraints(new_concentrations)?
} else {
new_concentrations
};
let final_concentrations = if self.config.enforce_conservation {
self.enforce_conservation_constraints(constrained_concentrations)?
} else {
constrained_concentrations
};
let final_reaction_rates = self.calculate_reaction_rates(&final_concentrations)?;
let new_state = ChemicalState {
concentrations: final_concentrations,
reaction_rates: final_reaction_rates.clone(),
time: t + self.config.dt,
};
let conservation_error = self.calculate_conservation_error(&new_state.concentrations);
let constraint_violation = Self::calculate_constraint_violation(&new_state.concentrations);
let stiffness_ratio = Self::estimate_stiffness_ratio(&final_reaction_rates);
self.reaction_rate_history.push(final_reaction_rates);
if self.reaction_rate_history.len() > 10 {
self.reaction_rate_history.remove(0);
}
self.previous_state = Some(state.clone());
let total_time = start_time.elapsed().as_secs_f64();
Ok(ChemicalResult {
state: new_state,
stats: ChemicalStats {
function_evaluations: 1,
jacobian_evaluations: 0,
newton_iterations: 0,
converged: true,
stiffness_ratio,
reaction_rate_time,
jacobian_time: total_time - reaction_rate_time,
},
success: true,
conservation_error,
constraint_violation,
})
}
fn calculate_reaction_rates(
&self,
concentrations: &Array1<f64>,
) -> IntegrateResult<Array1<f64>> {
let mut rates = Array1::zeros(self.properties.reactions.len());
for (i, reaction) in self.properties.reactions.iter().enumerate() {
rates[i] = self.calculate_single_reaction_rate(reaction, concentrations)?;
}
Ok(rates)
}
fn calculate_single_reaction_rate(
&self,
reaction: &Reaction,
concentrations: &Array1<f64>,
) -> IntegrateResult<f64> {
let mut rate = reaction.rate_constant;
if let Some(ref arrhenius) = reaction.arrhenius {
rate = arrhenius.pre_exponential
* (-arrhenius.activation_energy
/ (arrhenius.gas_constant * self.properties.temperature))
.exp();
}
match reaction.reaction_type {
ReactionType::Elementary => {
for &(species_idx, stoich) in &reaction.reactants {
if species_idx < concentrations.len() {
rate *= concentrations[species_idx].powf(stoich);
}
}
}
ReactionType::MichaelisMenten => {
if let Some(&(substrate_idx, _)) = reaction.reactants.first() {
if substrate_idx < concentrations.len() {
let substrate_conc = concentrations[substrate_idx];
let km = 0.1; rate = rate * substrate_conc / (km + substrate_conc);
}
}
}
ReactionType::Hill { coefficient } => {
if let Some(&(substrate_idx, _)) = reaction.reactants.first() {
if substrate_idx < concentrations.len() {
let substrate_conc = concentrations[substrate_idx];
let k_half = 0.1_f64; let substrate_n = substrate_conc.powf(coefficient);
let k_n = k_half.powf(coefficient);
rate = rate * substrate_n / (k_n + substrate_n);
}
}
}
ReactionType::CompetitiveInhibition => {
if let Some(&(substrate_idx, _)) = reaction.reactants.first() {
if substrate_idx < concentrations.len() {
let substrate_conc = concentrations[substrate_idx];
let km = 0.1; let ki = 0.05; let inhibitor_conc = if concentrations.len() > substrate_idx + 1 {
concentrations[substrate_idx + 1]
} else {
0.0
};
rate = rate * substrate_conc
/ (km * (1.0 + inhibitor_conc / ki) + substrate_conc);
}
}
}
ReactionType::NonCompetitiveInhibition => {
if let Some(&(substrate_idx, _)) = reaction.reactants.first() {
if substrate_idx < concentrations.len() {
let substrate_conc = concentrations[substrate_idx];
let km = 0.1; let ki = 0.05; let inhibitor_conc = if concentrations.len() > substrate_idx + 1 {
concentrations[substrate_idx + 1]
} else {
0.0
};
rate = rate * substrate_conc
/ ((km + substrate_conc) * (1.0 + inhibitor_conc / ki));
}
}
}
ReactionType::ProductInhibition => {
for &(species_idx, stoich) in &reaction.reactants {
if species_idx < concentrations.len() {
rate *= concentrations[species_idx].powf(stoich);
}
}
let ki = 0.1; for &(product_idx, _) in &reaction.products {
if product_idx < concentrations.len() {
rate /= 1.0 + concentrations[product_idx] / ki;
}
}
}
}
Ok(rate.max(0.0)) }
fn calculate_concentration_derivatives(
&self,
reaction_rates: &Array1<f64>,
) -> IntegrateResult<Array1<f64>> {
let mut derivatives = Array1::zeros(self.properties.num_species);
for (reaction_idx, rate) in reaction_rates.iter().enumerate() {
let reaction = &self.properties.reactions[reaction_idx];
for &(species_idx, stoich) in &reaction.reactants {
if species_idx < derivatives.len() {
derivatives[species_idx] -= stoich * rate;
}
}
for &(species_idx, stoich) in &reaction.products {
if species_idx < derivatives.len() {
derivatives[species_idx] += stoich * rate;
}
}
}
Ok(derivatives)
}
fn bdf2_step(
&self,
concentrations: &Array1<f64>,
derivatives: &Array1<f64>,
dt: f64,
) -> IntegrateResult<Array1<f64>> {
if let Some(ref prev_state) = self.previous_state {
let new_concentrations = concentrations + &(derivatives * dt);
Ok(new_concentrations)
} else {
self.implicit_euler_step(concentrations, derivatives, dt)
}
}
fn implicit_euler_step(
&self,
concentrations: &Array1<f64>,
derivatives: &Array1<f64>,
dt: f64,
) -> IntegrateResult<Array1<f64>> {
let new_concentrations = concentrations + &(derivatives * dt);
Ok(new_concentrations)
}
fn rosenbrock_step(
&self,
concentrations: &Array1<f64>,
derivatives: &Array1<f64>,
dt: f64,
) -> IntegrateResult<Array1<f64>> {
let new_concentrations = concentrations + &(derivatives * dt);
Ok(new_concentrations)
}
fn cash_karp_step(
&self,
concentrations: &Array1<f64>,
derivatives: &Array1<f64>,
dt: f64,
) -> IntegrateResult<Array1<f64>> {
let new_concentrations = concentrations + &(derivatives * dt);
Ok(new_concentrations)
}
fn adaptive_step(
&self,
concentrations: &Array1<f64>,
derivatives: &Array1<f64>,
dt: f64,
) -> IntegrateResult<Array1<f64>> {
let new_concentrations = concentrations + &(derivatives * dt);
Ok(new_concentrations)
}
fn enforce_positivity_constraints(
&self,
concentrations: Array1<f64>,
) -> IntegrateResult<Array1<f64>> {
Ok(concentrations.mapv(|x| x.max(0.0)))
}
fn enforce_conservation_constraints(
&self,
concentrations: Array1<f64>,
) -> IntegrateResult<Array1<f64>> {
Ok(concentrations)
}
fn calculate_conservation_error(&self, concentrations: &Array1<f64>) -> f64 {
if let Some(ref conservation_matrix) = self.properties.conservation_matrix {
let initial_conservation =
conservation_matrix.dot(&self.properties.initial_concentrations);
let current_conservation = conservation_matrix.dot(concentrations);
(¤t_conservation - &initial_conservation)
.iter()
.map(|x| x.abs())
.sum()
} else {
0.0
}
}
fn calculate_constraint_violation(concentrations: &Array1<f64>) -> f64 {
concentrations
.iter()
.map(|&x| if x < 0.0 { -x } else { 0.0 })
.sum()
}
fn estimate_stiffness_ratio(_reactionrates: &Array1<f64>) -> f64 {
if _reactionrates.len() < 2 {
return 1.0;
}
let max_rate = _reactionrates.iter().fold(0.0_f64, |a, &b| a.max(b.abs()));
let min_rate = _reactionrates.iter().fold(f64::INFINITY, |a, &b| {
if b.abs() > 1e-12 {
a.min(b.abs())
} else {
a
}
});
if min_rate > 0.0 && min_rate.is_finite() {
max_rate / min_rate
} else {
1.0
}
}
pub fn get_system_stats(&self) -> ChemicalSystemStats {
let total_concentration = self.properties.initial_concentrations.sum();
let num_reactions = self.properties.reactions.len();
let avg_rate = if !self.reaction_rate_history.is_empty() {
self.reaction_rate_history
.last()
.expect("Operation failed")
.sum()
/ num_reactions as f64
} else {
0.0
};
ChemicalSystemStats {
num_species: self.properties.num_species,
num_reactions,
total_concentration,
average_reaction_rate: avg_rate,
stiffness_estimate: Self::estimate_stiffness_ratio(
self.reaction_rate_history
.last()
.unwrap_or(&Array1::zeros(num_reactions)),
),
}
}
}
#[derive(Debug, Clone)]
pub struct ChemicalSystemStats {
pub num_species: usize,
pub num_reactions: usize,
pub total_concentration: f64,
pub average_reaction_rate: f64,
pub stiffness_estimate: f64,
}
pub mod systems {
use super::*;
pub fn first_order_reaction(
rate_constant: f64,
initial_a: f64,
initial_b: f64,
) -> (ChemicalConfig, ChemicalProperties, ChemicalState) {
let config = ChemicalConfig::default();
let reactions = vec![Reaction {
rate_constant,
reactants: vec![(0, 1.0)], products: vec![(1, 1.0)], reaction_type: ReactionType::Elementary,
arrhenius: None,
}];
let initial_concentrations = Array1::from_vec(vec![initial_a, initial_b]);
let properties = ChemicalProperties {
num_species: 2,
species_names: vec!["A".to_string(), "B".to_string()],
initial_concentrations: initial_concentrations.clone(),
reactions,
temperature: 298.15, volume: 1.0,
conservation_matrix: None,
};
let state = ChemicalState {
concentrations: initial_concentrations,
reaction_rates: Array1::zeros(1),
time: 0.0,
};
(config, properties, state)
}
pub fn reversible_reaction(
forward_rate: f64,
reverse_rate: f64,
initial_a: f64,
initial_b: f64,
) -> (ChemicalConfig, ChemicalProperties, ChemicalState) {
let config = ChemicalConfig::default();
let reactions = vec![
Reaction {
rate_constant: forward_rate,
reactants: vec![(0, 1.0)], products: vec![(1, 1.0)],
reaction_type: ReactionType::Elementary,
arrhenius: None,
},
Reaction {
rate_constant: reverse_rate,
reactants: vec![(1, 1.0)], products: vec![(0, 1.0)],
reaction_type: ReactionType::Elementary,
arrhenius: None,
},
];
let initial_concentrations = Array1::from_vec(vec![initial_a, initial_b]);
let properties = ChemicalProperties {
num_species: 2,
species_names: vec!["A".to_string(), "B".to_string()],
initial_concentrations: initial_concentrations.clone(),
reactions,
temperature: 298.15,
volume: 1.0,
conservation_matrix: Some(
Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).expect("Operation failed"),
),
};
let state = ChemicalState {
concentrations: initial_concentrations,
reaction_rates: Array1::zeros(2),
time: 0.0,
};
(config, properties, state)
}
pub fn enzyme_kinetics(
k1: f64,
k_minus_1: f64,
k2: f64,
initial_enzyme: f64,
initial_substrate: f64,
initial_product: f64,
) -> (ChemicalConfig, ChemicalProperties, ChemicalState) {
let config = ChemicalConfig {
system_type: ChemicalSystemType::EnzymeKinetics,
..Default::default()
};
let reactions = vec![
Reaction {
rate_constant: k1,
reactants: vec![(0, 1.0), (1, 1.0)], products: vec![(3, 1.0)],
reaction_type: ReactionType::Elementary,
arrhenius: None,
},
Reaction {
rate_constant: k_minus_1,
reactants: vec![(3, 1.0)], products: vec![(0, 1.0), (1, 1.0)],
reaction_type: ReactionType::Elementary,
arrhenius: None,
},
Reaction {
rate_constant: k2,
reactants: vec![(3, 1.0)], products: vec![(0, 1.0), (2, 1.0)],
reaction_type: ReactionType::Elementary,
arrhenius: None,
},
];
let initial_concentrations = Array1::from_vec(vec![
initial_enzyme, initial_substrate, initial_product, 0.0, ]);
let properties = ChemicalProperties {
num_species: 4,
species_names: vec![
"E".to_string(),
"S".to_string(),
"P".to_string(),
"ES".to_string(),
],
initial_concentrations: initial_concentrations.clone(),
reactions,
temperature: 310.15, volume: 1.0,
conservation_matrix: Some(
Array2::from_shape_vec(
(2, 4),
vec![
1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, ],
)
.expect("Operation failed"),
),
};
let state = ChemicalState {
concentrations: initial_concentrations,
reaction_rates: Array1::zeros(3),
time: 0.0,
};
(config, properties, state)
}
pub fn competitive_reactions(
k1: f64,
k2: f64,
initial_a: f64,
initial_b: f64,
initial_d: f64,
) -> (ChemicalConfig, ChemicalProperties, ChemicalState) {
let config = ChemicalConfig::default();
let reactions = vec![
Reaction {
rate_constant: k1,
reactants: vec![(0, 1.0), (1, 1.0)], products: vec![(2, 1.0)],
reaction_type: ReactionType::Elementary,
arrhenius: None,
},
Reaction {
rate_constant: k2,
reactants: vec![(0, 1.0), (3, 1.0)], products: vec![(4, 1.0)],
reaction_type: ReactionType::Elementary,
arrhenius: None,
},
];
let initial_concentrations = Array1::from_vec(vec![
initial_a, initial_b, 0.0, initial_d, 0.0, ]);
let properties = ChemicalProperties {
num_species: 5,
species_names: vec![
"A".to_string(),
"B".to_string(),
"C".to_string(),
"D".to_string(),
"E".to_string(),
],
initial_concentrations: initial_concentrations.clone(),
reactions,
temperature: 298.15,
volume: 1.0,
conservation_matrix: None,
};
let state = ChemicalState {
concentrations: initial_concentrations,
reaction_rates: Array1::zeros(2),
time: 0.0,
};
(config, properties, state)
}
pub fn stiff_reaction_system(
fast_rate: f64,
slow_rate: f64,
initial_concentrations: Vec<f64>,
) -> (ChemicalConfig, ChemicalProperties, ChemicalState) {
let config = ChemicalConfig {
stiff_method: StiffIntegrationMethod::BDF2,
dt: 0.001, ..Default::default()
};
let reactions = vec![
Reaction {
rate_constant: fast_rate,
reactants: vec![(0, 1.0)], products: vec![(1, 1.0)],
reaction_type: ReactionType::Elementary,
arrhenius: None,
},
Reaction {
rate_constant: slow_rate,
reactants: vec![(1, 1.0)], products: vec![(2, 1.0)],
reaction_type: ReactionType::Elementary,
arrhenius: None,
},
];
let initial_conc_array = Array1::from_vec(initial_concentrations.clone());
let properties = ChemicalProperties {
num_species: initial_concentrations.len(),
species_names: (0..initial_concentrations.len())
.map(|i| format!("Species_{i}"))
.collect(),
initial_concentrations: initial_conc_array.clone(),
reactions,
temperature: 298.15,
volume: 1.0,
conservation_matrix: None,
};
let state = ChemicalState {
concentrations: initial_conc_array,
reaction_rates: Array1::zeros(2),
time: 0.0,
};
(config, properties, state)
}
}
#[cfg(test)]
mod tests {
use crate::ode::{chemical::systems, ChemicalIntegrator};
use approx::assert_abs_diff_eq;
#[test]
fn test_first_order_reaction() {
let (config, properties, initial_state) = systems::first_order_reaction(0.1, 1.0, 0.0);
let mut integrator = ChemicalIntegrator::new(config, properties);
let result = integrator
.step(0.0, &initial_state)
.expect("Operation failed");
assert!(result.state.concentrations[0] < initial_state.concentrations[0]);
assert!(result.state.concentrations[1] > initial_state.concentrations[1]);
assert!(result.success);
}
#[test]
fn test_reversible_reaction() {
let (config, properties, initial_state) = systems::reversible_reaction(0.1, 0.05, 1.0, 0.0);
let mut integrator = ChemicalIntegrator::new(config, properties);
let result = integrator
.step(0.0, &initial_state)
.expect("Operation failed");
assert!(result.state.concentrations[0] < initial_state.concentrations[0]);
assert!(result.state.concentrations[1] > initial_state.concentrations[1]);
assert!(result.success);
}
#[test]
fn test_conservation() {
let (config, properties, initial_state) = systems::reversible_reaction(0.1, 0.05, 1.0, 0.0);
let mut integrator = ChemicalIntegrator::new(config, properties);
let result = integrator
.step(0.0, &initial_state)
.expect("Operation failed");
let initial_total = initial_state.concentrations.sum();
let final_total = result.state.concentrations.sum();
assert_abs_diff_eq!(initial_total, final_total, epsilon = 1e-10);
}
#[test]
fn test_enzyme_kinetics() {
let (config, properties, initial_state) = systems::enzyme_kinetics(
1.0, 0.1, 0.5, 0.1, 1.0, 0.0, );
let mut integrator = ChemicalIntegrator::new(config, properties);
let result = integrator
.step(0.0, &initial_state)
.expect("Operation failed");
assert!(result.success);
assert_eq!(result.state.concentrations.len(), 4); }
#[test]
fn test_positivity_constraints() {
let (mut config, properties, initial_state) = systems::first_order_reaction(10.0, 1.0, 0.0);
config.enforce_positivity = true;
config.dt = 1.0;
let mut integrator = ChemicalIntegrator::new(config, properties);
let result = integrator
.step(0.0, &initial_state)
.expect("Operation failed");
for &conc in result.state.concentrations.iter() {
assert!(conc >= 0.0, "Concentration should be non-negative: {conc}");
}
}
#[test]
fn test_stiff_system() {
let (config, properties, initial_state) = systems::stiff_reaction_system(
1000.0,
0.001, vec![1.0, 0.0, 0.0], );
let mut integrator = ChemicalIntegrator::new(config, properties);
let result = integrator
.step(0.0, &initial_state)
.expect("Operation failed");
assert!(result.stats.stiffness_ratio >= 1.0);
assert!(result.success);
}
#[test]
fn test_competitive_reactions() {
let (config, properties, initial_state) = systems::competitive_reactions(
0.1, 0.2, 1.0, 0.5, 0.3, );
let mut integrator = ChemicalIntegrator::new(config, properties);
let result = integrator
.step(0.0, &initial_state)
.expect("Operation failed");
assert!(result.state.concentrations[0] < initial_state.concentrations[0]);
assert!(result.success);
}
}