1use crate::error::{IntegrateError, IntegrateResult};
8use scirs2_core::ndarray::{Array1, Array2};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum ChemicalSystemType {
13 MassAction,
15 EnzymeKinetics,
17 MetabolicNetwork,
19 ReactionDiffusion,
21 Catalytic,
23}
24
25#[derive(Debug, Clone)]
27pub struct ChemicalConfig {
28 pub system_type: ChemicalSystemType,
30 pub dt: f64,
32 pub stiff_method: StiffIntegrationMethod,
34 pub jacobian_method: JacobianMethod,
36 pub concentration_tolerance: f64,
38 pub relative_tolerance: f64,
40 pub absolute_tolerance: f64,
42 pub enforce_positivity: bool,
44 pub enforce_conservation: bool,
46}
47
48impl Default for ChemicalConfig {
49 fn default() -> Self {
50 Self {
51 system_type: ChemicalSystemType::MassAction,
52 dt: 0.001,
53 stiff_method: StiffIntegrationMethod::BDF2,
54 jacobian_method: JacobianMethod::Analytical,
55 concentration_tolerance: 1e-12,
56 relative_tolerance: 1e-6,
57 absolute_tolerance: 1e-9,
58 enforce_positivity: true,
59 enforce_conservation: true,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq)]
66pub enum StiffIntegrationMethod {
67 BDF2,
69 Rosenbrock,
71 ImplicitEuler,
73 CashKarp,
75 Adaptive,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq)]
81pub enum JacobianMethod {
82 Analytical,
84 FiniteDifference,
86 ComplexStep,
88 AutomaticDifferentiation,
90}
91
92#[derive(Debug, Clone)]
94pub struct Reaction {
95 pub rate_constant: f64,
97 pub reactants: Vec<(usize, f64)>,
99 pub products: Vec<(usize, f64)>,
101 pub reaction_type: ReactionType,
103 pub arrhenius: Option<ArrheniusParams>,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq)]
109pub enum ReactionType {
110 Elementary,
112 MichaelisMenten,
114 Hill { coefficient: f64 },
116 CompetitiveInhibition,
118 NonCompetitiveInhibition,
120 ProductInhibition,
122}
123
124#[derive(Debug, Clone)]
126pub struct ArrheniusParams {
127 pub pre_exponential: f64,
129 pub activation_energy: f64,
131 pub gas_constant: f64,
133}
134
135#[derive(Debug, Clone)]
137pub struct ChemicalProperties {
138 pub num_species: usize,
140 pub species_names: Vec<String>,
142 pub initial_concentrations: Array1<f64>,
144 pub reactions: Vec<Reaction>,
146 pub temperature: f64,
148 pub volume: f64,
150 pub conservation_matrix: Option<Array2<f64>>,
152}
153
154#[derive(Debug, Clone)]
156pub struct ChemicalState {
157 pub concentrations: Array1<f64>,
159 pub reaction_rates: Array1<f64>,
161 pub time: f64,
163}
164
165#[derive(Debug, Clone)]
167pub struct ChemicalResult {
168 pub state: ChemicalState,
170 pub stats: ChemicalStats,
172 pub success: bool,
174 pub conservation_error: f64,
176 pub constraint_violation: f64,
178}
179
180#[derive(Debug, Clone)]
182pub struct ChemicalStats {
183 pub function_evaluations: usize,
185 pub jacobian_evaluations: usize,
187 pub newton_iterations: usize,
189 pub converged: bool,
191 pub stiffness_ratio: f64,
193 pub reaction_rate_time: f64,
195 pub jacobian_time: f64,
197}
198
199pub struct ChemicalIntegrator {
201 config: ChemicalConfig,
202 properties: ChemicalProperties,
203 previous_state: Option<ChemicalState>,
204 #[allow(dead_code)]
205 jacobian_cache: Option<Array2<f64>>,
206 reaction_rate_history: Vec<Array1<f64>>,
207}
208
209impl ChemicalIntegrator {
210 pub fn new(config: ChemicalConfig, properties: ChemicalProperties) -> Self {
212 Self {
213 config,
214 properties,
215 previous_state: None,
216 jacobian_cache: None,
217 reaction_rate_history: Vec::new(),
218 }
219 }
220
221 pub fn step(&mut self, t: f64, state: &ChemicalState) -> IntegrateResult<ChemicalResult> {
223 let start_time = std::time::Instant::now();
224
225 let reaction_rates = self.calculate_reaction_rates(&state.concentrations)?;
227 let reaction_rate_time = start_time.elapsed().as_secs_f64();
228
229 let derivatives = self.calculate_concentration_derivatives(&reaction_rates)?;
231
232 let new_concentrations = match self.config.stiff_method {
234 StiffIntegrationMethod::BDF2 => {
235 self.bdf2_step(&state.concentrations, &derivatives, self.config.dt)?
236 }
237 StiffIntegrationMethod::ImplicitEuler => {
238 self.implicit_euler_step(&state.concentrations, &derivatives, self.config.dt)?
239 }
240 StiffIntegrationMethod::Rosenbrock => {
241 self.rosenbrock_step(&state.concentrations, &derivatives, self.config.dt)?
242 }
243 StiffIntegrationMethod::CashKarp => {
244 self.cash_karp_step(&state.concentrations, &derivatives, self.config.dt)?
245 }
246 StiffIntegrationMethod::Adaptive => {
247 self.adaptive_step(&state.concentrations, &derivatives, self.config.dt)?
248 }
249 };
250
251 let constrained_concentrations = if self.config.enforce_positivity {
253 self.enforce_positivity_constraints(new_concentrations)?
254 } else {
255 new_concentrations
256 };
257
258 let final_concentrations = if self.config.enforce_conservation {
259 self.enforce_conservation_constraints(constrained_concentrations)?
260 } else {
261 constrained_concentrations
262 };
263
264 let final_reaction_rates = self.calculate_reaction_rates(&final_concentrations)?;
266
267 let new_state = ChemicalState {
269 concentrations: final_concentrations,
270 reaction_rates: final_reaction_rates.clone(),
271 time: t + self.config.dt,
272 };
273
274 let conservation_error = self.calculate_conservation_error(&new_state.concentrations);
276
277 let constraint_violation = Self::calculate_constraint_violation(&new_state.concentrations);
279
280 let stiffness_ratio = Self::estimate_stiffness_ratio(&final_reaction_rates);
282
283 self.reaction_rate_history.push(final_reaction_rates);
285 if self.reaction_rate_history.len() > 10 {
286 self.reaction_rate_history.remove(0);
287 }
288 self.previous_state = Some(state.clone());
289
290 let total_time = start_time.elapsed().as_secs_f64();
291
292 Ok(ChemicalResult {
293 state: new_state,
294 stats: ChemicalStats {
295 function_evaluations: 1,
296 jacobian_evaluations: 0,
297 newton_iterations: 0,
298 converged: true,
299 stiffness_ratio,
300 reaction_rate_time,
301 jacobian_time: total_time - reaction_rate_time,
302 },
303 success: true,
304 conservation_error,
305 constraint_violation,
306 })
307 }
308
309 fn calculate_reaction_rates(
311 &self,
312 concentrations: &Array1<f64>,
313 ) -> IntegrateResult<Array1<f64>> {
314 let mut rates = Array1::zeros(self.properties.reactions.len());
315
316 for (i, reaction) in self.properties.reactions.iter().enumerate() {
317 rates[i] = self.calculate_single_reaction_rate(reaction, concentrations)?;
318 }
319
320 Ok(rates)
321 }
322
323 fn calculate_single_reaction_rate(
325 &self,
326 reaction: &Reaction,
327 concentrations: &Array1<f64>,
328 ) -> IntegrateResult<f64> {
329 let mut rate = reaction.rate_constant;
330
331 if let Some(ref arrhenius) = reaction.arrhenius {
333 rate = arrhenius.pre_exponential
334 * (-arrhenius.activation_energy
335 / (arrhenius.gas_constant * self.properties.temperature))
336 .exp();
337 }
338
339 match reaction.reaction_type {
340 ReactionType::Elementary => {
341 for &(species_idx, stoich) in &reaction.reactants {
343 if species_idx < concentrations.len() {
344 rate *= concentrations[species_idx].powf(stoich);
345 }
346 }
347 }
348 ReactionType::MichaelisMenten => {
349 if let Some(&(substrate_idx, _)) = reaction.reactants.first() {
351 if substrate_idx < concentrations.len() {
352 let substrate_conc = concentrations[substrate_idx];
353 let km = 0.1; rate = rate * substrate_conc / (km + substrate_conc);
355 }
356 }
357 }
358 ReactionType::Hill { coefficient } => {
359 if let Some(&(substrate_idx, _)) = reaction.reactants.first() {
361 if substrate_idx < concentrations.len() {
362 let substrate_conc = concentrations[substrate_idx];
363 let k_half = 0.1_f64; let substrate_n = substrate_conc.powf(coefficient);
365 let k_n = k_half.powf(coefficient);
366 rate = rate * substrate_n / (k_n + substrate_n);
367 }
368 }
369 }
370 ReactionType::CompetitiveInhibition => {
371 if let Some(&(substrate_idx, _)) = reaction.reactants.first() {
373 if substrate_idx < concentrations.len() {
374 let substrate_conc = concentrations[substrate_idx];
375 let km = 0.1; let ki = 0.05; let inhibitor_conc = if concentrations.len() > substrate_idx + 1 {
378 concentrations[substrate_idx + 1]
379 } else {
380 0.0
381 };
382 rate = rate * substrate_conc
383 / (km * (1.0 + inhibitor_conc / ki) + substrate_conc);
384 }
385 }
386 }
387 ReactionType::NonCompetitiveInhibition => {
388 if let Some(&(substrate_idx, _)) = reaction.reactants.first() {
390 if substrate_idx < concentrations.len() {
391 let substrate_conc = concentrations[substrate_idx];
392 let km = 0.1; let ki = 0.05; let inhibitor_conc = if concentrations.len() > substrate_idx + 1 {
395 concentrations[substrate_idx + 1]
396 } else {
397 0.0
398 };
399 rate = rate * substrate_conc
400 / ((km + substrate_conc) * (1.0 + inhibitor_conc / ki));
401 }
402 }
403 }
404 ReactionType::ProductInhibition => {
405 for &(species_idx, stoich) in &reaction.reactants {
407 if species_idx < concentrations.len() {
408 rate *= concentrations[species_idx].powf(stoich);
409 }
410 }
411 let ki = 0.1; for &(product_idx, _) in &reaction.products {
413 if product_idx < concentrations.len() {
414 rate /= 1.0 + concentrations[product_idx] / ki;
415 }
416 }
417 }
418 }
419
420 Ok(rate.max(0.0)) }
422
423 fn calculate_concentration_derivatives(
425 &self,
426 reaction_rates: &Array1<f64>,
427 ) -> IntegrateResult<Array1<f64>> {
428 let mut derivatives = Array1::zeros(self.properties.num_species);
429
430 for (reaction_idx, rate) in reaction_rates.iter().enumerate() {
431 let reaction = &self.properties.reactions[reaction_idx];
432
433 for &(species_idx, stoich) in &reaction.reactants {
435 if species_idx < derivatives.len() {
436 derivatives[species_idx] -= stoich * rate;
437 }
438 }
439
440 for &(species_idx, stoich) in &reaction.products {
442 if species_idx < derivatives.len() {
443 derivatives[species_idx] += stoich * rate;
444 }
445 }
446 }
447
448 Ok(derivatives)
449 }
450
451 fn bdf2_step(
453 &self,
454 concentrations: &Array1<f64>,
455 derivatives: &Array1<f64>,
456 dt: f64,
457 ) -> IntegrateResult<Array1<f64>> {
458 if let Some(ref prev_state) = self.previous_state {
459 let new_concentrations = concentrations + &(derivatives * dt);
462 Ok(new_concentrations)
463 } else {
464 self.implicit_euler_step(concentrations, derivatives, dt)
466 }
467 }
468
469 fn implicit_euler_step(
471 &self,
472 concentrations: &Array1<f64>,
473 derivatives: &Array1<f64>,
474 dt: f64,
475 ) -> IntegrateResult<Array1<f64>> {
476 let new_concentrations = concentrations + &(derivatives * dt);
479 Ok(new_concentrations)
480 }
481
482 fn rosenbrock_step(
484 &self,
485 concentrations: &Array1<f64>,
486 derivatives: &Array1<f64>,
487 dt: f64,
488 ) -> IntegrateResult<Array1<f64>> {
489 let new_concentrations = concentrations + &(derivatives * dt);
491 Ok(new_concentrations)
492 }
493
494 fn cash_karp_step(
496 &self,
497 concentrations: &Array1<f64>,
498 derivatives: &Array1<f64>,
499 dt: f64,
500 ) -> IntegrateResult<Array1<f64>> {
501 let new_concentrations = concentrations + &(derivatives * dt);
503 Ok(new_concentrations)
504 }
505
506 fn adaptive_step(
508 &self,
509 concentrations: &Array1<f64>,
510 derivatives: &Array1<f64>,
511 dt: f64,
512 ) -> IntegrateResult<Array1<f64>> {
513 let new_concentrations = concentrations + &(derivatives * dt);
515 Ok(new_concentrations)
516 }
517
518 fn enforce_positivity_constraints(
520 &self,
521 concentrations: Array1<f64>,
522 ) -> IntegrateResult<Array1<f64>> {
523 Ok(concentrations.mapv(|x| x.max(0.0)))
524 }
525
526 fn enforce_conservation_constraints(
528 &self,
529 concentrations: Array1<f64>,
530 ) -> IntegrateResult<Array1<f64>> {
531 Ok(concentrations)
534 }
535
536 fn calculate_conservation_error(&self, concentrations: &Array1<f64>) -> f64 {
538 if let Some(ref conservation_matrix) = self.properties.conservation_matrix {
539 let initial_conservation =
541 conservation_matrix.dot(&self.properties.initial_concentrations);
542 let current_conservation = conservation_matrix.dot(concentrations);
543 (¤t_conservation - &initial_conservation)
544 .iter()
545 .map(|x| x.abs())
546 .sum()
547 } else {
548 0.0
549 }
550 }
551
552 fn calculate_constraint_violation(concentrations: &Array1<f64>) -> f64 {
554 concentrations
556 .iter()
557 .map(|&x| if x < 0.0 { -x } else { 0.0 })
558 .sum()
559 }
560
561 fn estimate_stiffness_ratio(_reactionrates: &Array1<f64>) -> f64 {
563 if _reactionrates.len() < 2 {
564 return 1.0;
565 }
566
567 let max_rate = _reactionrates.iter().fold(0.0_f64, |a, &b| a.max(b.abs()));
568 let min_rate = _reactionrates.iter().fold(f64::INFINITY, |a, &b| {
569 if b.abs() > 1e-12 {
570 a.min(b.abs())
571 } else {
572 a
573 }
574 });
575
576 if min_rate > 0.0 && min_rate.is_finite() {
577 max_rate / min_rate
578 } else {
579 1.0
580 }
581 }
582
583 pub fn get_system_stats(&self) -> ChemicalSystemStats {
585 let total_concentration = self.properties.initial_concentrations.sum();
586 let num_reactions = self.properties.reactions.len();
587 let avg_rate = if !self.reaction_rate_history.is_empty() {
588 self.reaction_rate_history.last().unwrap().sum() / num_reactions as f64
589 } else {
590 0.0
591 };
592
593 ChemicalSystemStats {
594 num_species: self.properties.num_species,
595 num_reactions,
596 total_concentration,
597 average_reaction_rate: avg_rate,
598 stiffness_estimate: Self::estimate_stiffness_ratio(
599 self.reaction_rate_history
600 .last()
601 .unwrap_or(&Array1::zeros(num_reactions)),
602 ),
603 }
604 }
605}
606
607#[derive(Debug, Clone)]
609pub struct ChemicalSystemStats {
610 pub num_species: usize,
611 pub num_reactions: usize,
612 pub total_concentration: f64,
613 pub average_reaction_rate: f64,
614 pub stiffness_estimate: f64,
615}
616
617pub mod systems {
619 use super::*;
620
621 pub fn first_order_reaction(
623 rate_constant: f64,
624 initial_a: f64,
625 initial_b: f64,
626 ) -> (ChemicalConfig, ChemicalProperties, ChemicalState) {
627 let config = ChemicalConfig::default();
628
629 let reactions = vec![Reaction {
630 rate_constant,
631 reactants: vec![(0, 1.0)], products: vec![(1, 1.0)], reaction_type: ReactionType::Elementary,
634 arrhenius: None,
635 }];
636
637 let initial_concentrations = Array1::from_vec(vec![initial_a, initial_b]);
638
639 let properties = ChemicalProperties {
640 num_species: 2,
641 species_names: vec!["A".to_string(), "B".to_string()],
642 initial_concentrations: initial_concentrations.clone(),
643 reactions,
644 temperature: 298.15, volume: 1.0,
646 conservation_matrix: None,
647 };
648
649 let state = ChemicalState {
650 concentrations: initial_concentrations,
651 reaction_rates: Array1::zeros(1),
652 time: 0.0,
653 };
654
655 (config, properties, state)
656 }
657
658 pub fn reversible_reaction(
660 forward_rate: f64,
661 reverse_rate: f64,
662 initial_a: f64,
663 initial_b: f64,
664 ) -> (ChemicalConfig, ChemicalProperties, ChemicalState) {
665 let config = ChemicalConfig::default();
666
667 let reactions = vec![
668 Reaction {
669 rate_constant: forward_rate,
670 reactants: vec![(0, 1.0)], products: vec![(1, 1.0)],
672 reaction_type: ReactionType::Elementary,
673 arrhenius: None,
674 },
675 Reaction {
676 rate_constant: reverse_rate,
677 reactants: vec![(1, 1.0)], products: vec![(0, 1.0)],
679 reaction_type: ReactionType::Elementary,
680 arrhenius: None,
681 },
682 ];
683
684 let initial_concentrations = Array1::from_vec(vec![initial_a, initial_b]);
685
686 let properties = ChemicalProperties {
687 num_species: 2,
688 species_names: vec!["A".to_string(), "B".to_string()],
689 initial_concentrations: initial_concentrations.clone(),
690 reactions,
691 temperature: 298.15,
692 volume: 1.0,
693 conservation_matrix: Some(Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).unwrap()),
694 };
695
696 let state = ChemicalState {
697 concentrations: initial_concentrations,
698 reaction_rates: Array1::zeros(2),
699 time: 0.0,
700 };
701
702 (config, properties, state)
703 }
704
705 pub fn enzyme_kinetics(
707 k1: f64,
708 k_minus_1: f64,
709 k2: f64,
710 initial_enzyme: f64,
711 initial_substrate: f64,
712 initial_product: f64,
713 ) -> (ChemicalConfig, ChemicalProperties, ChemicalState) {
714 let config = ChemicalConfig {
715 system_type: ChemicalSystemType::EnzymeKinetics,
716 ..Default::default()
717 };
718
719 let reactions = vec![
720 Reaction {
721 rate_constant: k1,
722 reactants: vec![(0, 1.0), (1, 1.0)], products: vec![(3, 1.0)],
724 reaction_type: ReactionType::Elementary,
725 arrhenius: None,
726 },
727 Reaction {
728 rate_constant: k_minus_1,
729 reactants: vec![(3, 1.0)], products: vec![(0, 1.0), (1, 1.0)],
731 reaction_type: ReactionType::Elementary,
732 arrhenius: None,
733 },
734 Reaction {
735 rate_constant: k2,
736 reactants: vec![(3, 1.0)], products: vec![(0, 1.0), (2, 1.0)],
738 reaction_type: ReactionType::Elementary,
739 arrhenius: None,
740 },
741 ];
742
743 let initial_concentrations = Array1::from_vec(vec![
744 initial_enzyme, initial_substrate, initial_product, 0.0, ]);
749
750 let properties = ChemicalProperties {
751 num_species: 4,
752 species_names: vec![
753 "E".to_string(),
754 "S".to_string(),
755 "P".to_string(),
756 "ES".to_string(),
757 ],
758 initial_concentrations: initial_concentrations.clone(),
759 reactions,
760 temperature: 310.15, volume: 1.0,
762 conservation_matrix: Some(
763 Array2::from_shape_vec(
764 (2, 4),
765 vec![
766 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, ],
769 )
770 .unwrap(),
771 ),
772 };
773
774 let state = ChemicalState {
775 concentrations: initial_concentrations,
776 reaction_rates: Array1::zeros(3),
777 time: 0.0,
778 };
779
780 (config, properties, state)
781 }
782
783 pub fn competitive_reactions(
785 k1: f64,
786 k2: f64,
787 initial_a: f64,
788 initial_b: f64,
789 initial_d: f64,
790 ) -> (ChemicalConfig, ChemicalProperties, ChemicalState) {
791 let config = ChemicalConfig::default();
792
793 let reactions = vec![
794 Reaction {
795 rate_constant: k1,
796 reactants: vec![(0, 1.0), (1, 1.0)], products: vec![(2, 1.0)],
798 reaction_type: ReactionType::Elementary,
799 arrhenius: None,
800 },
801 Reaction {
802 rate_constant: k2,
803 reactants: vec![(0, 1.0), (3, 1.0)], products: vec![(4, 1.0)],
805 reaction_type: ReactionType::Elementary,
806 arrhenius: None,
807 },
808 ];
809
810 let initial_concentrations = Array1::from_vec(vec![
811 initial_a, initial_b, 0.0, initial_d, 0.0, ]);
817
818 let properties = ChemicalProperties {
819 num_species: 5,
820 species_names: vec![
821 "A".to_string(),
822 "B".to_string(),
823 "C".to_string(),
824 "D".to_string(),
825 "E".to_string(),
826 ],
827 initial_concentrations: initial_concentrations.clone(),
828 reactions,
829 temperature: 298.15,
830 volume: 1.0,
831 conservation_matrix: None,
832 };
833
834 let state = ChemicalState {
835 concentrations: initial_concentrations,
836 reaction_rates: Array1::zeros(2),
837 time: 0.0,
838 };
839
840 (config, properties, state)
841 }
842
843 pub fn stiff_reaction_system(
845 fast_rate: f64,
846 slow_rate: f64,
847 initial_concentrations: Vec<f64>,
848 ) -> (ChemicalConfig, ChemicalProperties, ChemicalState) {
849 let config = ChemicalConfig {
850 stiff_method: StiffIntegrationMethod::BDF2,
851 dt: 0.001, ..Default::default()
853 };
854
855 let reactions = vec![
856 Reaction {
857 rate_constant: fast_rate,
858 reactants: vec![(0, 1.0)], products: vec![(1, 1.0)],
860 reaction_type: ReactionType::Elementary,
861 arrhenius: None,
862 },
863 Reaction {
864 rate_constant: slow_rate,
865 reactants: vec![(1, 1.0)], products: vec![(2, 1.0)],
867 reaction_type: ReactionType::Elementary,
868 arrhenius: None,
869 },
870 ];
871
872 let initial_conc_array = Array1::from_vec(initial_concentrations.clone());
873
874 let properties = ChemicalProperties {
875 num_species: initial_concentrations.len(),
876 species_names: (0..initial_concentrations.len())
877 .map(|i| format!("Species_{i}"))
878 .collect(),
879 initial_concentrations: initial_conc_array.clone(),
880 reactions,
881 temperature: 298.15,
882 volume: 1.0,
883 conservation_matrix: None,
884 };
885
886 let state = ChemicalState {
887 concentrations: initial_conc_array,
888 reaction_rates: Array1::zeros(2),
889 time: 0.0,
890 };
891
892 (config, properties, state)
893 }
894}
895
896#[cfg(test)]
897mod tests {
898 use crate::ode::{chemical::systems, ChemicalIntegrator};
899 use approx::assert_abs_diff_eq;
900
901 #[test]
902 fn test_first_order_reaction() {
903 let (config, properties, initial_state) = systems::first_order_reaction(0.1, 1.0, 0.0);
904 let mut integrator = ChemicalIntegrator::new(config, properties);
905
906 let result = integrator.step(0.0, &initial_state).unwrap();
907
908 assert!(result.state.concentrations[0] < initial_state.concentrations[0]);
910 assert!(result.state.concentrations[1] > initial_state.concentrations[1]);
911 assert!(result.success);
912 }
913
914 #[test]
915 fn test_reversible_reaction() {
916 let (config, properties, initial_state) = systems::reversible_reaction(0.1, 0.05, 1.0, 0.0);
917 let mut integrator = ChemicalIntegrator::new(config, properties);
918
919 let result = integrator.step(0.0, &initial_state).unwrap();
920
921 assert!(result.state.concentrations[0] < initial_state.concentrations[0]);
923 assert!(result.state.concentrations[1] > initial_state.concentrations[1]);
924 assert!(result.success);
925 }
926
927 #[test]
928 fn test_conservation() {
929 let (config, properties, initial_state) = systems::reversible_reaction(0.1, 0.05, 1.0, 0.0);
930 let mut integrator = ChemicalIntegrator::new(config, properties);
931
932 let result = integrator.step(0.0, &initial_state).unwrap();
933
934 let initial_total = initial_state.concentrations.sum();
936 let final_total = result.state.concentrations.sum();
937 assert_abs_diff_eq!(initial_total, final_total, epsilon = 1e-10);
938 }
939
940 #[test]
941 fn test_enzyme_kinetics() {
942 let (config, properties, initial_state) = systems::enzyme_kinetics(
943 1.0, 0.1, 0.5, 0.1, 1.0, 0.0, );
946 let mut integrator = ChemicalIntegrator::new(config, properties);
947
948 let result = integrator.step(0.0, &initial_state).unwrap();
949
950 assert!(result.success);
952 assert_eq!(result.state.concentrations.len(), 4); }
954
955 #[test]
956 fn test_positivity_constraints() {
957 let (mut config, properties, initial_state) = systems::first_order_reaction(10.0, 1.0, 0.0);
958 config.enforce_positivity = true;
959 config.dt = 1.0; let mut integrator = ChemicalIntegrator::new(config, properties);
962 let result = integrator.step(0.0, &initial_state).unwrap();
963
964 for &conc in result.state.concentrations.iter() {
966 assert!(conc >= 0.0, "Concentration should be non-negative: {conc}");
967 }
968 }
969
970 #[test]
971 fn test_stiff_system() {
972 let (config, properties, initial_state) = systems::stiff_reaction_system(
973 1000.0,
974 0.001, vec![1.0, 0.0, 0.0], );
977 let mut integrator = ChemicalIntegrator::new(config, properties);
978
979 let result = integrator.step(0.0, &initial_state).unwrap();
980
981 assert!(result.stats.stiffness_ratio >= 1.0);
985 assert!(result.success);
986 }
987
988 #[test]
989 fn test_competitive_reactions() {
990 let (config, properties, initial_state) = systems::competitive_reactions(
991 0.1, 0.2, 1.0, 0.5, 0.3, );
994 let mut integrator = ChemicalIntegrator::new(config, properties);
995
996 let result = integrator.step(0.0, &initial_state).unwrap();
997
998 assert!(result.state.concentrations[0] < initial_state.concentrations[0]);
1000 assert!(result.success);
1001 }
1002}