use RustedSciThe::Utils::plots::plots_terminal;
use RustedSciThe::numerical::ODE_api2::{SolverParam, SolverType, UniversalODESolver};
use RustedSciThe::symbolic::symbolic_engine::Expr;
use nalgebra::{DMatrix, DVector};
use std::collections::HashMap;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use tabled::{Table, Tabled};
#[allow(non_upper_case_globals)]
pub const two: Expr = Expr::Const(2.0);
#[allow(non_upper_case_globals)]
pub const three: Expr = Expr::Const(3.0);
#[allow(non_upper_case_globals)]
pub const four: Expr = Expr::Const(4.0);
#[allow(non_upper_case_globals)]
pub const one: Expr = Expr::Const(1.0);
#[allow(non_upper_case_globals)]
pub const half: Expr = Expr::Const(0.5);
#[allow(non_upper_case_globals)]
pub const neg: Expr = Expr::Const(-1.0);
pub fn A2() -> Expr {
let a = Expr::Var("a".to_owned());
let f = two * (one - a.clone()) * Expr::Pow(Box::new(-Expr::ln(one - a)), Box::new(-half));
return f;
}
pub fn A3() -> Expr {
let a = Expr::Var("a".to_owned());
let f = three
* (one - a.clone())
* Expr::Pow(
Box::new(-Expr::ln(one - a)),
Box::new(Expr::Const(-2.0 / 3.0)),
);
return f;
}
pub fn A4() -> Expr {
let a = Expr::Var("a".to_owned());
let f = four
* (one - a.clone())
* Expr::Pow(
Box::new(-Expr::ln(one - a)),
Box::new(Expr::Const(-3.0 / 4.0)),
);
return f;
}
pub fn R2() -> Expr {
let a = Expr::Var("a".to_owned());
return Expr::Pow(Box::new(one - a), Box::new(half));
}
pub fn R3() -> Expr {
let a = Expr::Var("a".to_owned());
return Expr::Pow(Box::new(one - a), Box::new(Expr::Const(2.0 / 3.0)));
}
pub fn D1() -> Expr {
let a = Expr::Var("a".to_owned());
return Expr::Pow(Box::new(a), Box::new(neg));
}
pub fn D2() -> Expr {
let a = Expr::Var("a".to_owned());
return Expr::Pow(Box::new(-Expr::ln(one - a)), Box::new(neg));
}
pub fn D3() -> Expr {
let a = Expr::Var("a".to_owned());
return Expr::Const(1.5)
* Expr::Pow(Box::new(one - a.clone()), Box::new(Expr::Const(2.0 / 3.0)))
* Expr::Pow(
Box::new(one - Expr::Pow(Box::new(one - a), Box::new(Expr::Const(1.0 / 3.0)))),
Box::new(neg),
);
}
pub fn D4() -> Expr {
let a = Expr::Var("a".to_owned());
return Expr::Const(1.5)
* Expr::Pow(
Box::new(Expr::Pow(Box::new(one - a), Box::new(Expr::Const(-1.0 / 3.0))) - one),
Box::new(neg),
);
}
pub fn P2_3() -> Expr {
let a = Expr::Var("a".to_owned());
return Expr::Pow(Box::new(a), Box::new(Expr::Const(-0.5)));
}
pub fn P2() -> Expr {
let a = Expr::Var("a".to_owned());
return Expr::Pow(Box::new(a), Box::new(half));
}
pub fn P3() -> Expr {
let a = Expr::Var("a".to_owned());
return Expr::Pow(Box::new(a), Box::new(Expr::Const(0.75)));
}
pub fn F1() -> Expr {
let a = Expr::Var("a".to_owned());
return one - a;
}
pub fn F2() -> Expr {
let a = Expr::Var("a".to_owned());
return Expr::Pow(Box::new(one - a), Box::new(two));
}
pub fn F3() -> Expr {
let a = Expr::Var("a".to_owned());
return Expr::Pow(Box::new(one - a), Box::new(three));
}
pub fn Sestak_Berggen(m: f64, n: f64, p: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
let n = Expr::Const(n);
let p = Expr::Const(p);
return Expr::Pow(Box::new(one - a.clone()), Box::new(m))
* Expr::Pow(Box::new(a.clone()), Box::new(n))
* Expr::Pow(Box::new(-Expr::ln(one - a)), Box::new(p));
}
pub fn SB_j(m: f64, n: f64, p: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
let n = Expr::Const(n);
let p = Expr::Const(p);
return Expr::Pow(Box::new(Expr::ln(one - a.clone())), Box::new(p.clone()))
* (n.clone()
* Expr::Pow(Box::new(one - a.clone()), Box::new(m.clone()))
* Expr::Pow(Box::new(a.clone()), Box::new(n.clone() - one))
- m.clone()
* Expr::Pow(Box::new(one - a.clone()), Box::new(m.clone() - one))
* Expr::Pow(Box::new(a.clone()), Box::new(n.clone())))
- p.clone()
* Expr::Pow(Box::new(Expr::ln(one - a.clone())), Box::new(p - one))
* Expr::Pow(Box::new(one - a.clone()), Box::new(m - one))
* Expr::Pow(Box::new(a), Box::new(n));
}
pub fn Johnson_Mehl_Avrami(m: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
return m.clone()
* (one - a.clone())
* Expr::Pow(Box::new(-Expr::ln(one - a)), Box::new(one - one / m));
}
pub fn JMA_jac(m: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
return m.clone()
* (Expr::Pow(
Box::new(-Expr::ln(one - a.clone())),
Box::new(-one / m.clone()),
) / m.clone()
- Expr::Pow(
Box::new(-Expr::ln(one - a.clone())),
Box::new(-one / m.clone()),
)
- Expr::Pow(Box::new(-Expr::ln(one - a)), Box::new(one - one / m)));
}
pub fn Accelerating_model(m: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
return m.clone() * Expr::Pow(Box::new(a), Box::new(one - one / m));
}
pub fn Ac_jac(m: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
return (one - one / m.clone()) * m.clone() * Expr::Pow(Box::new(a), Box::new(-one / m));
}
pub fn Decelerating_model(m: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
return Expr::Pow(Box::new(one - a), Box::new(m));
}
pub fn Dec_jac(m: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
return -m.clone() * Expr::Pow(Box::new(one - a), Box::new(m - one));
}
pub fn Prout_Tompkins_ext(m: f64, n: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
let n = Expr::Const(n);
return Expr::Pow(Box::new(one - a.clone()), Box::new(m)) * Expr::Pow(Box::new(a), Box::new(n));
}
pub fn PTe_jac(m: f64, n: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
let n = Expr::Const(n);
return n.clone()
* Expr::Pow(Box::new(one - a.clone()), Box::new(m.clone()))
* Expr::Pow(Box::new(a.clone()), Box::new(n.clone() - one))
- m.clone()
* Expr::Pow(Box::new(one - a.clone()), Box::new(m - one))
* Expr::Pow(Box::new(a), Box::new(n));
}
pub fn Sestak_Berggren_trunc_with_param(m: f64, n: f64, c: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
let n = Expr::Const(n);
let c = Expr::Const(c);
return c
* Expr::Pow(Box::new(one - a.clone()), Box::new(m))
* Expr::Pow(Box::new(a), Box::new(n));
}
pub fn SBtp_jac(m: f64, n: f64, c: f64) -> Expr {
let a = Expr::Var("a".to_owned());
let m = Expr::Const(m);
let n = Expr::Const(n);
let c = Expr::Const(c);
return c
* (n.clone()
* Expr::Pow(Box::new(one - a.clone()), Box::new(m.clone()))
* Expr::Pow(Box::new(a.clone()), Box::new(n.clone() - one))
- m.clone()
* Expr::Pow(Box::new(one - a.clone()), Box::new(m - one))
* Expr::Pow(Box::new(a), Box::new(n)));
}
#[derive(Debug, Clone, PartialEq, EnumIter)]
pub enum KineticModelNames {
A2,
A3,
A4,
R2,
R3,
D1,
D2,
D3,
D4,
P2_3,
P2,
P3,
F1,
F2,
F3,
SB,
JMA,
Ac,
Dec,
PTe,
SBtp,
}
impl KineticModelNames {
pub fn map_of_names_and_formulas(&self) -> String {
match self {
KineticModelNames::A2 => "2*(1-a)*(-ln(1-a))^(1/2)".to_string(),
KineticModelNames::A3 => "3*(1-a)*(-ln(1-a))^(2/3)".to_string(),
KineticModelNames::A4 => "4*(1-a)*(-ln(1-a))^(3/4)".to_string(),
KineticModelNames::R2 => "(1-a)^(1/2)".to_string(),
KineticModelNames::R3 => "(1-a)^(1/3)".to_string(),
KineticModelNames::D1 => "a^(-1)".to_string(),
KineticModelNames::D2 => "(-ln(1-a))^(-1)".to_string(),
KineticModelNames::D3 => "1.5*(1-a)^(2/3)*(1-a^(1/3))^(-1)".to_string(),
KineticModelNames::D4 => "(3/2)*((1-a)^(-1/3)-1)^-1".to_string(),
KineticModelNames::P2_3 => "a^(-0.5)".to_string(),
KineticModelNames::P2 => "a^(1/2)".to_string(),
KineticModelNames::P3 => "a^(0.75)".to_string(),
KineticModelNames::F1 => "(1-a)".to_string(),
KineticModelNames::F2 => "(1-a)^(2)".to_string(),
KineticModelNames::F3 => "(1-a)^(3)".to_string(),
KineticModelNames::SB => " (1-a)^m*a^n*(-ln(1-a))^p".to_string(),
KineticModelNames::JMA => "m*(1-a)*(-ln(1-a))^(1-1/m)".to_string(),
KineticModelNames::Ac => "m*a^(1-1/m)".to_string(),
KineticModelNames::Dec => "(1-a)^m".to_string(),
KineticModelNames::PTe => "(1-a)^m * a^n".to_string(),
KineticModelNames::SBtp => "c*(1-a)^m*a^n".to_string(),
}
}
pub fn required_params(&self) -> Vec<String> {
match self {
KineticModelNames::A2
| KineticModelNames::A3
| KineticModelNames::A4
| KineticModelNames::R2
| KineticModelNames::R3
| KineticModelNames::D1
| KineticModelNames::D2
| KineticModelNames::D3
| KineticModelNames::D4
| KineticModelNames::P2_3
| KineticModelNames::P2
| KineticModelNames::P3
| KineticModelNames::F1
| KineticModelNames::F2
| KineticModelNames::F3 => vec![],
KineticModelNames::SB => vec!["m".to_string(), "n".to_string(), "p".to_string()],
KineticModelNames::JMA => vec!["m".to_string()],
KineticModelNames::Ac => vec!["m".to_string()],
KineticModelNames::Dec => vec!["m".to_string()],
KineticModelNames::PTe => vec!["m".to_string(), "n".to_string()],
KineticModelNames::SBtp => vec!["m".to_string(), "n".to_string(), "c".to_string()],
}
}
pub fn pretty_print() {
#[derive(Tabled)]
struct ModelRow {
code: String,
expression: String,
}
let data: Vec<ModelRow> = KineticModelNames::iter()
.map(|model| ModelRow {
code: format!("{:?}", model),
expression: model.map_of_names_and_formulas(),
})
.collect();
let mut binding = Table::new(data);
let table = binding.with(tabled::settings::Style::rounded());
println!("{}", table);
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum KineticModel {
A2,
A3,
A4,
R2,
R3,
D1,
D2,
D3,
D4,
P2_3,
P2,
P3,
F1,
F2,
F3,
SB(f64, f64, f64),
JMA(f64),
Ac(f64),
Dec(f64),
PTe(f64, f64),
SBtp(f64, f64, f64),
}
impl KineticModel {
pub fn required_params(&self) -> Vec<String> {
match self {
KineticModel::A2
| KineticModel::A3
| KineticModel::A4
| KineticModel::R2
| KineticModel::R3
| KineticModel::D1
| KineticModel::D2
| KineticModel::D3
| KineticModel::D4
| KineticModel::P2_3
| KineticModel::P2
| KineticModel::P3
| KineticModel::F1
| KineticModel::F2
| KineticModel::F3 => vec![],
KineticModel::SB(_, _, _) => vec!["m".to_string(), "n".to_string(), "p".to_string()],
KineticModel::JMA(_) => vec!["m".to_string()],
KineticModel::Ac(_) => vec!["m".to_string()],
KineticModel::Dec(_) => vec!["m".to_string()],
KineticModel::PTe(_, _) => vec!["m".to_string(), "n".to_string()],
KineticModel::SBtp(_, _, _) => vec!["m".to_string(), "n".to_string(), "c".to_string()],
}
}
pub fn from_name_and_params(name: KineticModelNames, params: Vec<f64>) -> Result<Self, String> {
match name {
KineticModelNames::A2 => Ok(KineticModel::A2),
KineticModelNames::A3 => Ok(KineticModel::A3),
KineticModelNames::A4 => Ok(KineticModel::A4),
KineticModelNames::R2 => Ok(KineticModel::R2),
KineticModelNames::R3 => Ok(KineticModel::R3),
KineticModelNames::D1 => Ok(KineticModel::D1),
KineticModelNames::D2 => Ok(KineticModel::D2),
KineticModelNames::D3 => Ok(KineticModel::D3),
KineticModelNames::D4 => Ok(KineticModel::D4),
KineticModelNames::P2_3 => Ok(KineticModel::P2_3),
KineticModelNames::P2 => Ok(KineticModel::P2),
KineticModelNames::P3 => Ok(KineticModel::P3),
KineticModelNames::F1 => Ok(KineticModel::F1),
KineticModelNames::F2 => Ok(KineticModel::F2),
KineticModelNames::F3 => Ok(KineticModel::F3),
KineticModelNames::SB => {
if params.len() != 3 {
return Err("SB model requires 3 parameters (m, n, p)".to_string());
}
Ok(KineticModel::SB(params[0], params[1], params[2]))
}
KineticModelNames::JMA => {
if params.len() != 1 {
return Err("JMA model requires 1 parameter (m)".to_string());
}
Ok(KineticModel::JMA(params[0]))
}
KineticModelNames::Ac => {
if params.len() != 1 {
return Err("Ac model requires 1 parameter (m)".to_string());
}
Ok(KineticModel::Ac(params[0]))
}
KineticModelNames::Dec => {
if params.len() != 1 {
return Err("Dec model requires 1 parameter (m)".to_string());
}
Ok(KineticModel::Dec(params[0]))
}
KineticModelNames::PTe => {
if params.len() != 2 {
return Err("PTe model requires 2 parameters (m, n)".to_string());
}
Ok(KineticModel::PTe(params[0], params[1]))
}
KineticModelNames::SBtp => {
if params.len() != 3 {
return Err("SBtp model requires 3 parameters (m, n, c)".to_string());
}
Ok(KineticModel::SBtp(params[0], params[1], params[2]))
}
}
}
pub fn get_expr(&self) -> Expr {
match self {
KineticModel::A2 => A2(),
KineticModel::A3 => A3(),
KineticModel::A4 => A4(),
KineticModel::R2 => R2(),
KineticModel::R3 => R3(),
KineticModel::D1 => D1(),
KineticModel::D2 => D2(),
KineticModel::D3 => D3(),
KineticModel::D4 => D4(),
KineticModel::P2_3 => P2_3(),
KineticModel::P2 => P2(),
KineticModel::P3 => P3(),
KineticModel::F1 => F1(),
KineticModel::F2 => F2(),
KineticModel::F3 => F3(),
KineticModel::SB(m, n, p) => Sestak_Berggen(*m, *n, *p),
KineticModel::JMA(m) => Johnson_Mehl_Avrami(*m),
KineticModel::Ac(m) => Accelerating_model(*m),
KineticModel::Dec(m) => Decelerating_model(*m),
KineticModel::PTe(m, n) => Prout_Tompkins_ext(*m, *n),
KineticModel::SBtp(m, n, c) => Sestak_Berggren_trunc_with_param(*m, *n, *c),
}
}
fn get_jac(&self) -> Expr {
match self {
KineticModel::SB(m, n, p) => SB_j(*m, *n, *p),
KineticModel::JMA(m) => JMA_jac(*m),
KineticModel::Ac(m) => Ac_jac(*m),
KineticModel::Dec(m) => Dec_jac(*m),
KineticModel::PTe(m, n) => PTe_jac(*m, *n),
KineticModel::SBtp(m, n, c) => SBtp_jac(*m, *n, *c),
_ => self.get_expr().diff("a"),
}
}
}
pub fn create_kinetic_model(
name: KineticModelNames,
params: Vec<f64>,
) -> Result<(KineticModel, Expr), String> {
match KineticModel::from_name_and_params(name, params) {
Ok(kinmodel) => {
let jac = kinmodel.get_jac();
Ok((kinmodel, jac))
}
Err(e) => Err(e),
}
}
pub struct KineticModelIVP {
solver: Option<UniversalODESolver>,
solver_params: HashMap<String, SolverParam>,
t_final: f64,
beta: f64,
T0: f64,
E: f64,
A: f64,
kinmodel: Option<KineticModel>,
kin_expression: Expr,
jac: Expr,
solvertype: SolverType,
stop_condition: Option<HashMap<String, f64>>,
}
impl KineticModelIVP {
pub fn new(solvertype: SolverType) -> Self {
let map_of_params = HashMap::from([
("step_size".to_owned(), SolverParam::Float(1e-3)),
("tolerance".to_owned(), SolverParam::Float(1e-3)),
("max_iterations".to_owned(), SolverParam::Int(100000)),
("rtol".to_owned(), SolverParam::Float(1e-3)),
("atol".to_owned(), SolverParam::Float(1e-3)),
("max_step".to_owned(), SolverParam::Float(0.1)),
("first_step".to_owned(), SolverParam::OptionalFloat(None)),
("vectorized".to_owned(), SolverParam::Bool(false)),
("jac_sparsity".to_owned(), SolverParam::OptionalMatrix(None)),
("parallel".to_owned(), SolverParam::Bool(true)),
]);
Self {
solver: None,
solver_params: map_of_params,
t_final: 0.0,
beta: 0.0,
T0: 0.0,
E: 0.0,
A: 0.0,
kinmodel: None,
kin_expression: Expr::Const(0.0),
jac: Expr::Const(0.0),
solvertype: solvertype,
stop_condition: None,
}
}
pub fn set_stop_condition(&mut self, condition: Option<HashMap<String, f64>>) {
self.stop_condition = condition;
}
pub fn set_problem(
&mut self,
t_final: f64,
beta: f64,
T0: f64,
E: f64,
A: f64,
) -> Result<(), String> {
if t_final <= 0.0 {
return Err("t_final must be positive".to_string());
}
if beta <= 0.0 {
return Err("beta must be positive".to_string());
}
if T0 <= 0.0 {
return Err("T0 must be positive".to_string());
}
if E <= 0.0 {
return Err("E must be positive".to_string());
}
if A <= 0.0 {
return Err("A must be positive".to_string());
}
self.t_final = t_final;
self.beta = beta;
self.T0 = T0;
self.E = E;
self.A = A;
Ok(())
}
pub fn set_solver_params(&mut self, params: HashMap<String, SolverParam>) {
self.solver_params = params;
}
pub fn set_model(&mut self, name: KineticModelNames, params: Vec<f64>) -> Result<(), String> {
let (kinmodel, jac) = create_kinetic_model(name, params)?;
self.kinmodel = Some(kinmodel.clone());
let arrhenius = self.arrhenius();
self.kin_expression = arrhenius * kinmodel.get_expr();
self.jac = jac;
Ok(())
}
pub fn check_task(&self) -> Result<(), String> {
if self.t_final <= 0.0 {
return Err("t_final not set or invalid".to_string());
}
if self.beta <= 0.0 {
return Err("beta not set or invalid".to_string());
}
if self.E <= 0.0 {
return Err("E not set or invalid".to_string());
}
if self.A <= 0.0 {
return Err("A not set or invalid".to_string());
}
if self.T0 <= 0.0 {
return Err("T0 not set or invalid".to_string());
}
if self.kinmodel.is_none() {
return Err("kinetic model not set".to_string());
}
if self.kin_expression == Expr::Const(0.0) {
return Err("kinetic expression not initialized".to_string());
}
if self.jac == Expr::Const(0.0) {
return Err("jacobian not initialized".to_string());
}
Ok(())
}
pub fn solve(&mut self) -> Result<(), String> {
self.check_task()?;
let y0 = DVector::from_vec(vec![1e-5]);
let mut ode = UniversalODESolver::new(
vec![self.kin_expression.clone()],
vec!["a".to_owned()],
"t".to_owned(),
self.solvertype.clone(),
0.0,
y0,
self.t_final,
);
ode.set_parameters(self.solver_params.clone());
if let Some(stop_condition) = self.stop_condition.clone() {
ode.set_stop_condition(stop_condition)
} else {
let stop_condition = HashMap::from([("a".to_owned(), 0.999)]);
ode.set_stop_condition(stop_condition)
}
ode.initialize();
ode.solve();
self.solver = Some(ode);
Ok(())
}
pub fn plot(&self) -> Result<(), String> {
let ode = self
.solver
.as_ref()
.ok_or("Solver not initialized. Call solve() first.")?;
ode.plot_result();
Ok(())
}
pub fn plot_in_terminal(&self) {
let (t, a) = self.solver.as_ref().unwrap().get_result();
plots_terminal(
"t".to_string(),
vec!["a".to_string()],
t.unwrap(),
a.unwrap(),
)
}
pub fn get_result(&self) -> Result<(DVector<f64>, DMatrix<f64>), String> {
let (t, a) = self.solver.as_ref().unwrap().get_result();
if let (Some(t), Some(a)) = (t, a) {
Ok((t, a))
} else {
Err("no results found!".to_string())
}
}
pub fn save_result(&self) -> Result<(), String> {
let ode = self
.solver
.as_ref()
.ok_or("Solver not initialized. Call solve() first.")?;
ode.save_result()
.map_err(|e| format!("Failed to save result: {:?}", e))?;
Ok(())
}
fn arrhenius(&self) -> Expr {
let E = Expr::Const(self.E);
let A = Expr::Const(self.A);
let beta = Expr::Const(self.beta);
let R_sym = Expr::Const(8.31446261815324);
let T0 = Expr::Const(self.T0);
let t = Expr::Var("t".to_string());
let k = A * Expr::Exp(Box::new(-E / (R_sym * (T0 + t * beta))));
k
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parameter_free_models() {
let models = [
KineticModelNames::A2,
KineticModelNames::A3,
KineticModelNames::A4,
KineticModelNames::R2,
KineticModelNames::R3,
KineticModelNames::D1,
KineticModelNames::D2,
KineticModelNames::D3,
KineticModelNames::D4,
KineticModelNames::P2_3,
KineticModelNames::P2,
KineticModelNames::P3,
KineticModelNames::F1,
KineticModelNames::F2,
KineticModelNames::F3,
];
for model_name in models {
let result = create_kinetic_model(model_name.clone(), vec![]);
assert!(result.is_ok(), "Failed to create model {:?}", model_name);
let (kinetic_model, jacobian) = result.unwrap();
let expr = kinetic_model.get_expr();
assert_ne!(format!("{:?}", expr), "Const(0.0)");
assert_ne!(format!("{:?}", jacobian), "Const(0.0)");
}
}
#[test]
fn test_parameterized_models() {
let result = create_kinetic_model(KineticModelNames::SB, vec![1.0, 2.0, 0.5]);
assert!(result.is_ok());
let (model, jac) = result.unwrap();
let sbjac = SB_j(1.0, 2.0, 0.5);
assert!(matches!(model, KineticModel::SB(1.0, 2.0, 0.5)));
assert_eq!(jac, sbjac);
let result = create_kinetic_model(KineticModelNames::JMA, vec![2.0]);
assert!(result.is_ok());
let (model, jac) = result.unwrap();
let jmajac = JMA_jac(2.0);
assert!(matches!(model, KineticModel::JMA(2.0)));
assert_eq!(jac, jmajac);
let result = create_kinetic_model(KineticModelNames::Ac, vec![1.5]);
assert!(result.is_ok());
let (model, jac) = result.unwrap();
assert!(matches!(model, KineticModel::Ac(1.5)));
let acjac = Ac_jac(1.5);
assert_eq!(jac, acjac);
let result = create_kinetic_model(KineticModelNames::Dec, vec![3.0]);
assert!(result.is_ok());
let (model, jac) = result.unwrap();
let decjac = Dec_jac(3.0);
assert!(matches!(model, KineticModel::Dec(3.0)));
assert_eq!(jac, decjac);
let result = create_kinetic_model(KineticModelNames::PTe, vec![2.0, 1.0]);
assert!(result.is_ok());
let (model, jac) = result.unwrap();
let ptejac = PTe_jac(2.0, 1.0);
assert!(matches!(model, KineticModel::PTe(2.0, 1.0)));
assert_eq!(jac, ptejac);
let result = create_kinetic_model(KineticModelNames::SBtp, vec![1.0, 2.0, 0.8]);
assert!(result.is_ok());
let (model, jac) = result.unwrap();
assert!(matches!(model, KineticModel::SBtp(1.0, 2.0, 0.8)));
let sbtpjac = SBtp_jac(1.0, 2.0, 0.8);
assert_eq!(jac, sbtpjac);
}
#[test]
fn test_parameter_validation() {
assert!(create_kinetic_model(KineticModelNames::SB, vec![1.0]).is_err());
assert!(create_kinetic_model(KineticModelNames::JMA, vec![]).is_err());
assert!(create_kinetic_model(KineticModelNames::PTe, vec![1.0]).is_err());
assert!(create_kinetic_model(KineticModelNames::SBtp, vec![1.0, 2.0]).is_err());
}
#[test]
fn test_jacobian_consistency() {
let test_cases = [
(KineticModelNames::SB, vec![1.0, 2.0, 0.5]),
(KineticModelNames::JMA, vec![2.0]),
(KineticModelNames::Ac, vec![1.5]),
(KineticModelNames::Dec, vec![3.0]),
(KineticModelNames::PTe, vec![2.0, 1.0]),
(KineticModelNames::SBtp, vec![1.0, 2.0, 0.8]),
];
for (model_name, params) in test_cases {
let result = create_kinetic_model(model_name.clone(), params);
assert!(result.is_ok(), "Failed to create model {:?}", model_name);
let (kinetic_model, jacobian) = result.unwrap();
assert_ne!(format!("{:?}", jacobian), "Const(0.0)");
match kinetic_model {
KineticModel::SB(_, _, _) => {
assert!(
format!("{:?}", jacobian).contains("Pow")
|| format!("{:?}", jacobian).contains("ln")
);
}
KineticModel::JMA(_) => {
assert!(
format!("{:?}", jacobian).contains("Pow")
|| format!("{:?}", jacobian).contains("ln")
);
}
_ => {}
}
}
}
#[test]
fn test_individual_functions() {
let expr = A2();
println!("{:?}", expr);
assert!(format!("{:?}", expr).contains("Pow"));
assert!(format!("{:?}", expr).contains("Ln"));
let sb_expr = Sestak_Berggen(1.0, 2.0, 0.5);
assert!(format!("{:?}", sb_expr).contains("Pow"));
let jma_expr = Johnson_Mehl_Avrami(2.0);
assert!(format!("{:?}", jma_expr).contains("Pow"));
assert!(format!("{:?}", jma_expr).contains("Ln"));
}
#[test]
fn test_model_names_mapping() {
for model in KineticModelNames::iter() {
let mapping = model.map_of_names_and_formulas();
assert!(!mapping.is_empty(), "Empty mapping for {:?}", model);
assert!(
mapping.contains("a"),
"Mapping should contain variable 'a' for {:?}",
model
);
}
}
#[test]
fn test_expression_evaluation() {
let models_with_params = [
(KineticModelNames::A2, vec![]),
(KineticModelNames::F1, vec![]),
(KineticModelNames::SB, vec![1.0, 2.0, 0.5]),
(KineticModelNames::JMA, vec![2.0]),
];
for (model_name, params) in models_with_params {
let result = create_kinetic_model(model_name.clone(), params);
assert!(result.is_ok());
let (kinetic_model, jacobian) = result.unwrap();
let expr = kinetic_model.get_expr();
let expr_str = format!("{:?}", expr);
let _jac_str = format!("{:?}", jacobian);
assert!(
expr_str.contains("Var(\"a\")"),
"Expression should contain variable 'a' for {:?}",
model_name
);
}
}
#[test]
fn test_constants() {
assert_eq!(two, Expr::Const(2.0));
assert_eq!(three, Expr::Const(3.0));
assert_eq!(four, Expr::Const(4.0));
assert_eq!(one, Expr::Const(1.0));
assert_eq!(half, Expr::Const(0.5));
assert_eq!(neg, Expr::Const(-1.0));
}
#[test]
fn test_create_kinetic_model_error_handling() {
let result = create_kinetic_model(KineticModelNames::SB, vec![1.0, 2.0]); assert!(result.is_err());
let result = create_kinetic_model(KineticModelNames::JMA, vec![1.0, 2.0]); assert!(result.is_err());
}
}
#[cfg(test)]
mod kinetic_model_ivp_tests {
use super::*;
use RustedSciThe::numerical::Radau::Radau_main::RadauOrder;
use std::time::Instant;
#[test]
fn test_new() {
let ivp = KineticModelIVP::new(SolverType::BDF);
assert!(ivp.solver.is_none());
assert_eq!(ivp.t_final, 0.0);
assert_eq!(ivp.beta, 0.0);
assert_eq!(ivp.T0, 0.0);
assert_eq!(ivp.E, 0.0);
assert_eq!(ivp.A, 0.0);
assert!(ivp.kinmodel.is_none());
assert_eq!(ivp.kin_expression, Expr::Const(0.0));
assert_eq!(ivp.jac, Expr::Const(0.0));
assert!(matches!(ivp.solvertype, SolverType::BDF));
assert!(ivp.solver_params.contains_key("step_size"));
assert!(ivp.solver_params.contains_key("tolerance"));
assert!(ivp.solver_params.contains_key("max_iterations"));
}
#[test]
fn test_set_problem() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
let _ = ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6);
assert_eq!(ivp.t_final, 100.0);
assert_eq!(ivp.beta, 10.0);
assert_eq!(ivp.T0, 298.15);
assert_eq!(ivp.E, 50000.0);
assert_eq!(ivp.A, 1e6);
}
#[test]
fn test_set_model() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
let _ = ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6);
let _ = ivp.set_model(KineticModelNames::A2, vec![]);
assert!(ivp.kinmodel.is_some());
assert!(matches!(ivp.kinmodel.as_ref().unwrap(), KineticModel::A2));
assert_ne!(ivp.kin_expression, Expr::Const(0.0));
assert_ne!(ivp.jac, Expr::Const(0.0));
let _ = ivp.set_model(KineticModelNames::JMA, vec![2.0]);
assert!(matches!(
ivp.kinmodel.as_ref().unwrap(),
KineticModel::JMA(2.0)
));
println!("{:?}", ivp.kin_expression);
assert_ne!(ivp.kin_expression, Expr::Const(0.0));
assert_ne!(ivp.jac, Expr::Const(0.0));
}
#[test]
fn test_check_task_success() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
let _ = ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6);
let _ = ivp.set_model(KineticModelNames::A2, vec![]);
let _ = ivp.check_task();
}
#[test]
fn test_check_task_fail_no_problem() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
let _ = ivp.set_model(KineticModelNames::A2, vec![]);
let r = ivp.check_task();
match r {
Ok(_) => {}
Err(e) => {
assert_eq!(e, "t_final not set or invalid")
}
}
}
#[test]
fn test_check_task_fail_no_model() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
let _ = ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6);
let r = ivp.check_task();
match r {
Ok(_) => {}
Err(e) => {
assert_eq!(e, "kinetic model not set")
}
}
}
#[test]
fn test_complete_workflow() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
assert!(ivp.kinmodel.is_none());
let _ = ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6);
assert_eq!(ivp.t_final, 100.0);
let _ = ivp.set_model(KineticModelNames::JMA, vec![2.0]);
assert!(ivp.kinmodel.is_some());
let _ = ivp.check_task(); }
#[test]
fn test_arrhenius_expression() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
let _ = ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6);
let arrhenius = ivp.arrhenius();
let expr_str = format!("{:?}", arrhenius);
assert!(expr_str.contains("Exp"));
assert!(expr_str.contains("Const(1000000.0)")); assert!(expr_str.contains("Const(50000.0)")); }
#[test]
fn test_different_solver_types() {
let solver_types = [SolverType::BDF, SolverType::BackwardEuler, SolverType::BDF];
for solver_type in solver_types {
let mut ivp = KineticModelIVP::new(solver_type.clone());
assert!(matches!(ivp.solvertype, ref _solver_type));
let _ = ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6);
let _ = ivp.set_model(KineticModelNames::F1, vec![]);
let _ = ivp.check_task(); }
}
#[test]
fn test_multiple_models() {
let test_cases = [
(KineticModelNames::A2, vec![]),
(KineticModelNames::F1, vec![]),
(KineticModelNames::JMA, vec![2.0]),
(KineticModelNames::SB, vec![1.0, 2.0, 0.5]),
(KineticModelNames::PTe, vec![2.0, 1.0]),
];
for (model_name, params) in test_cases {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
let _ = ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6);
let _ = ivp.set_model(model_name.clone(), params);
assert!(ivp.kinmodel.is_some());
assert_ne!(ivp.kin_expression, Expr::Const(0.0));
let _ = ivp.check_task();
}
}
#[test]
fn test_parameter_validation_in_set_model() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6).unwrap();
let result = ivp.set_model(KineticModelNames::JMA, vec![1.0, 2.0]); assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("JMA model requires 1 parameter")
);
}
#[test]
fn test_kinetic_expression_contains_arrhenius() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
let _ = ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6);
let _ = ivp.set_model(KineticModelNames::F1, vec![]);
let expr_str = format!("{:?}", ivp.kin_expression);
assert!(expr_str.contains("Mul") || expr_str.contains("*"));
assert!(expr_str.contains("Exp"));
assert!(expr_str.contains("Var(\"a\")"));
}
#[test]
fn solve_with_rk45() {
let mut ivp = KineticModelIVP::new(SolverType::NonStiff("RK45".to_owned()));
let _ = ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6);
let _ = ivp.set_model(KineticModelNames::F1, vec![]);
let _ = ivp.check_task();
let _ = ivp.solve();
let _ = ivp.plot();
}
#[test]
fn solve_with_radau() {
let t = Instant::now();
let mut ivp = KineticModelIVP::new(SolverType::Radau(RadauOrder::Order7));
let _ = ivp.set_problem(10.0, 10.0, 298.15, 50000.0, 1e6);
let _ = ivp.set_model(KineticModelNames::D3, vec![]);
let _ = ivp.check_task();
let _ = ivp.solve();
println!("Time elapsed: {:?}", t.elapsed().as_secs());
let _ = ivp.plot();
}
#[test]
fn solve_with_be() {
let mut ivp = KineticModelIVP::new(SolverType::BackwardEuler);
let _ = ivp.set_problem(10.0, 10.0, 298.15, 50000.0, 1e6);
let _ = ivp.set_model(KineticModelNames::D3, vec![]);
let _ = ivp.check_task();
let _ = ivp.solve();
let _ = ivp.plot();
}
}
#[cfg(test)]
mod error_handling_tests {
use std::time::Instant;
use super::*;
use RustedSciThe::numerical::Radau::Radau_main::RadauOrder;
#[test]
fn test_set_problem_validation() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
assert!(ivp.set_problem(-1.0, 10.0, 298.15, 50000.0, 1e6).is_err());
assert!(ivp.set_problem(100.0, -10.0, 298.15, 50000.0, 1e6).is_err());
assert!(ivp.set_problem(100.0, 10.0, -298.15, 50000.0, 1e6).is_err());
assert!(ivp.set_problem(100.0, 10.0, 298.15, -50000.0, 1e6).is_err());
assert!(ivp.set_problem(100.0, 10.0, 298.15, 50000.0, -1e6).is_err());
assert!(ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6).is_ok());
}
#[test]
fn test_set_model_error_propagation() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6).unwrap();
let result = ivp.set_model(KineticModelNames::JMA, vec![1.0, 2.0]);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("JMA model requires 1 parameter")
);
let result = ivp.set_model(KineticModelNames::JMA, vec![2.0]);
assert!(result.is_ok());
}
#[test]
fn test_check_task_detailed_errors() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
let result = ivp.check_task();
assert!(result.is_err());
assert!(result.unwrap_err().contains("t_final not set"));
ivp.set_problem(100.0, 10.0, 298.15, 50000.0, 1e6).unwrap();
let result = ivp.check_task();
assert!(result.is_err());
assert!(result.unwrap_err().contains("kinetic model not set"));
ivp.set_model(KineticModelNames::A2, vec![]).unwrap();
let result = ivp.check_task();
assert!(result.is_ok());
}
#[test]
fn test_solve_without_setup() {
let mut ivp = KineticModelIVP::new(SolverType::BDF);
let result = ivp.solve();
assert!(result.is_err());
assert!(result.unwrap_err().contains("not set"));
}
#[test]
fn test_gnuplot_without_solve() {
let ivp = KineticModelIVP::new(SolverType::BDF);
let result = ivp.plot();
assert!(result.is_err());
assert!(result.unwrap_err().contains("Solver not initialized"));
}
#[test]
fn test_save_result_without_solve() {
let ivp = KineticModelIVP::new(SolverType::BDF);
let result = ivp.save_result();
assert!(result.is_err());
assert!(result.unwrap_err().contains("Solver not initialized"));
}
#[test]
fn test_complete_error_free_workflow() {
let t = Instant::now();
let mut ivp = KineticModelIVP::new(SolverType::Radau(RadauOrder::Order7));
assert!(ivp.set_problem(50.0, 10.0, 298.15, 50000.0, 1e6).is_ok());
let stop_condition = HashMap::from([("a".to_owned(), 0.99)]);
ivp.set_stop_condition(Some(stop_condition));
assert!(ivp.set_model(KineticModelNames::F1, vec![]).is_ok());
assert!(ivp.check_task().is_ok());
assert!(ivp.solve().is_ok());
assert!(ivp.plot().is_ok());
assert!(ivp.save_result().is_ok());
println!("Time taken: {:?}", t.elapsed().as_secs());
ivp.plot_in_terminal()
}
}