use crate::Kinetics::User_reactions::KinData;
use crate::Kinetics::mechfinder_api::ReactionData;
use crate::ReactorsBVP::SimpleReactorBVP::ReactorError;
use crate::ReactorsBVP::reactor_BVP_utils::{
BoundsConfig, ScalingConfig, ToleranceConfig, create_bounds_map, create_tolerance_map,
};
use RustedSciThe::symbolic::symbolic_engine::Expr;
use log::info;
use nalgebra::{DMatrix, DVector};
use std::collections::HashMap;
use std::fmt;
pub const R_G: f64 = 8.314;
#[derive(Debug, Clone)]
pub struct SolutionQuality {
pub energy_balane_error_abs: f64,
pub energy_balane_error_rel: f64,
pub sum_of_mass_fractions: Vec<(usize, f64)>,
pub atomic_mass_balance_error: Vec<(usize, f64)>,
}
impl Default for SolutionQuality {
fn default() -> Self {
Self {
energy_balane_error_abs: 0.0,
energy_balane_error_rel: 0.0,
sum_of_mass_fractions: Vec::new(),
atomic_mass_balance_error: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct FastElemReact {
pub eq: String,
pub A: f64,
pub n: f64,
pub E: f64,
pub Q: f64,
}
#[derive(Debug, Clone)]
pub struct SimpleReactorTask {
pub problem_name: Option<String>,
pub problem_description: Option<String>,
pub kindata: KinData,
pub thermal_effects: Vec<f64>,
pub P: f64,
pub Tm: f64,
pub Cp: f64,
pub boundary_condition: HashMap<String, f64>,
pub Lambda: f64,
pub ro: f64,
pub m: f64,
pub scaling: ScalingConfig,
pub L: f64,
pub T_scaling: Expr,
pub M: f64,
pub Pe_q: f64,
pub map_eq_rate: HashMap<String, Expr>,
pub map_of_equations: HashMap<String, (String, Expr)>,
pub heat_release: Expr,
pub solver: IVPSolver,
}
#[derive(Debug, Clone)]
pub struct IVPSolver {
pub arg_name: String,
pub x_range: (f64, f64),
pub unknowns: Vec<String>,
pub eq_system: Vec<Expr>,
pub BorderConditions: HashMap<String, (usize, f64)>,
pub solution: Option<DMatrix<f64>>,
pub x_mesh: Option<DVector<f64>>,
pub quality: SolutionQuality,
}
impl Default for IVPSolver {
fn default() -> Self {
Self {
arg_name: "x".to_string(),
x_range: (0.0, 1.0),
unknowns: Vec::new(),
eq_system: Vec::new(),
BorderConditions: HashMap::new(),
solution: None,
x_mesh: None,
quality: SolutionQuality::default(),
}
}
}
impl IVPSolver {
pub fn get_solution(&self) -> Option<&DMatrix<f64>> {
self.solution.as_ref()
}
pub fn debug_solution(&self) {
if let Some(solution) = &self.solution {
println!("\n=== SOLUTION DEBUG ===");
println!(
"Solution matrix shape: {} x {}",
solution.nrows(),
solution.ncols()
);
println!("Unknowns: {:?}", self.unknowns);
for (i, var_name) in self.unknowns.iter().enumerate() {
if i < solution.ncols() {
let col = solution.column(i);
println!(
"{}: [first...{:.6}, {:.6}, {:.6}, ... last: {:.6}, {:.6}, {:.6}]",
var_name,
col[0],
col[1.min(col.len() - 1)],
col[2.min(col.len() - 1)],
col[col.len() - 3],
col[col.len() - 2],
col[col.len() - 1]
);
}
}
println!("=== END DEBUG ===\n");
}
}
}
impl SimpleReactorTask {
pub fn new() -> Self {
Self {
problem_name: None,
problem_description: None,
kindata: KinData::new(),
thermal_effects: Vec::new(),
P: 0.0,
Tm: 0.0,
Cp: 0.0,
boundary_condition: HashMap::new(),
Lambda: 0.0,
ro: 0.0,
m: 0.0,
scaling: ScalingConfig::default(),
L: 1.0,
T_scaling: Expr::Const(0.0),
M: 0.0,
Pe_q: 0.0,
map_eq_rate: HashMap::new(),
map_of_equations: HashMap::new(),
heat_release: Expr::Const(0.0),
solver: IVPSolver::default(),
}
}
pub fn create_tolerance_map_for_system(
&self,
tolerance_config: HashMap<String, f64>,
) -> HashMap<String, f64> {
create_tolerance_map(tolerance_config, &self.kindata.substances)
}
pub fn create_bounds_map_for_system(
&self,
bounds_config: HashMap<String, (f64, f64)>,
) -> HashMap<String, (f64, f64)> {
create_bounds_map(bounds_config, &self.kindata.substances)
}
pub fn set_scaling(&mut self, scaling: ScalingConfig) -> Result<(), ReactorError> {
scaling.validate()?;
self.scaling = scaling;
Ok(())
}
pub fn set_scaling_values(
&mut self,
dT: f64,
L: f64,
T_scale: f64,
) -> Result<(), ReactorError> {
let scaling = ScalingConfig::new(dT, L, T_scale);
self.set_scaling(scaling)
}
pub fn set_problem_name(&mut self, name: &str) {
self.problem_name = Some(name.to_string());
}
pub fn set_problem_description(&mut self, description: &str) {
self.problem_description = Some(description.to_string());
}
pub fn set_boundary_conditions(&mut self, conditions: HashMap<String, f64>) {
self.boundary_condition = conditions;
}
pub fn set_parameters(
&mut self,
thermal_effects: Vec<f64>,
P: f64,
Tm: f64,
Cp: f64,
boundary_condition: HashMap<String, f64>,
Lambda: f64,
Diffusion: HashMap<String, f64>,
m: f64,
scaling: ScalingConfig,
) {
self.thermal_effects = thermal_effects;
self.P = P;
self.Tm = Tm;
self.Cp = Cp;
self.boundary_condition = boundary_condition;
self.Lambda = Lambda;
self.m = m;
self.scaling = scaling;
}
pub fn setup_IVP(&mut self) -> Result<(), ReactorError> {
self.check_task()?;
info!("task checked!");
self.scaling_processing()?;
info!("scaling processed!");
self.kinetic_processing()?;
info!("kinetics processed!");
self.mean_molar_mass()?;
info!("mean molar mass calculated");
self.peclet_numbers()?;
info!("Peclet numbers calculated");
self.create_IVP_equations()?;
info!("IVP equations created");
self.set_solver_BC()?;
info!("Boundary conditions created!");
self.check_before_solution()?;
info!("IVP setup completed!");
self.pretty_print_task();
self.pretty_print_equations();
self.pretty_print_reaction_rates();
Ok(())
}
pub fn set_transport_properties(&mut self, lambda: f64, cp: f64) {
self.Lambda = lambda;
self.Cp = cp;
}
pub fn set_operating_conditions(&mut self, pressure: f64, temperature: f64, mass_flow: f64) {
self.P = pressure;
self.Tm = temperature;
self.m = mass_flow;
}
pub fn set_thermal_effects(&mut self, thermal_effects: Vec<f64>) {
self.thermal_effects = thermal_effects;
}
pub fn fast_react_set(&mut self, vec_of_maps: Vec<FastElemReact>) -> Result<(), ReactorError> {
let mut eq_vec: Vec<String> = Vec::new();
let mut elementary_reaction_vec = Vec::new();
let mut Q_vec = Vec::new();
for (idx, map_of_reactiondata) in vec_of_maps.iter().enumerate() {
let eq = if !map_of_reactiondata.eq.is_empty() {
map_of_reactiondata.eq.clone()
} else {
return Err(ReactorError::MissingData(format!(
"No equation in input hashmap at index {}",
idx
)));
};
let A = map_of_reactiondata.A;
let n = map_of_reactiondata.n;
let E = map_of_reactiondata.E;
let Q = map_of_reactiondata.Q;
if A.is_nan() {
return Err(ReactorError::MissingData(format!(
"Missing Arrhenius parameter 'A' in input hashmap at index {}",
idx
)));
}
if n.is_nan() {
return Err(ReactorError::MissingData(format!(
"Missing Arrhenius parameter 'n' in input hashmap at index {}",
idx
)));
}
if E.is_nan() {
return Err(ReactorError::MissingData(format!(
"Missing Arrhenius parameter 'E' in input hashmap at index {}",
idx
)));
}
if Q.is_nan() {
return Err(ReactorError::MissingData(format!(
"Missing Arrhenius parameter 'Q' in input hashmap at index {}",
idx
)));
}
let arrenius = vec![A, n, E];
let reactdata = ReactionData::new_elementary(eq.clone(), arrenius, None);
eq_vec.push(eq);
Q_vec.push(Q);
elementary_reaction_vec.push(reactdata);
}
let mut kindata = KinData::new();
kindata.vec_of_equations = eq_vec;
kindata.vec_of_reaction_data = Some(elementary_reaction_vec);
self.kindata = kindata;
self.thermal_effects = Q_vec;
Ok(())
}
pub fn check_task(&self) -> Result<(), ReactorError> {
if self.P <= 0.0 {
return Err(ReactorError::MissingData("P must be positive".to_string()));
}
if self.Tm <= 0.0 {
return Err(ReactorError::MissingData("Tm must be positive".to_string()));
}
if self.Cp <= 0.0 {
return Err(ReactorError::MissingData("Cp must be positive".to_string()));
}
if self.Lambda <= 0.0 {
return Err(ReactorError::MissingData(
"Lambda must be positive".to_string(),
));
}
if self.m <= 0.0 {
return Err(ReactorError::MissingData("m must be positive".to_string()));
}
if self.thermal_effects.len() != self.kindata.vec_of_equations.len() {
return Err(ReactorError::InvalidConfiguration(
"Thermal effects length must match number of reactions".to_string(),
));
}
self.scaling.validate()?;
if !self.boundary_condition.contains_key("T") {
return Err(ReactorError::MissingData(
"Missing T in boundary conditions".to_string(),
));
}
for substance in &self.kindata.substances {
if !self.boundary_condition.contains_key(substance) {
return Err(ReactorError::MissingData(format!(
"Missing boundary condition for {}",
substance
)));
}
}
Ok(())
}
pub fn check_before_solution(&self) -> Result<(), ReactorError> {
let n_substances = self.kindata.substances.len();
let expected_len = n_substances + 2;
if self.solver.eq_system.len() != expected_len {
return Err(ReactorError::InvalidConfiguration(format!(
"eq_system length {} != expected {}",
self.solver.eq_system.len(),
expected_len
)));
}
if self.solver.unknowns.len() != expected_len {
return Err(ReactorError::InvalidConfiguration(format!(
"unknowns length {} != expected {}",
self.solver.unknowns.len(),
expected_len
)));
}
if self.map_of_equations.len() != expected_len {
return Err(ReactorError::InvalidConfiguration(format!(
"map_of_equations length {} != expected {}",
self.map_of_equations.len(),
expected_len
)));
}
if self.solver.BorderConditions.len() != expected_len {
return Err(ReactorError::InvalidConfiguration(format!(
"BorderConditions length {} != expected {}",
self.solver.BorderConditions.len(),
expected_len
)));
}
if self.M <= 0.0 {
return Err(ReactorError::InvalidConfiguration(
"M must be positive".to_string(),
));
}
if self.Pe_q <= 0.0 {
return Err(ReactorError::InvalidConfiguration(
"Pe_q must be positive".to_string(),
));
}
Ok(())
}
pub fn kinetic_processing(&mut self) -> Result<(), ReactorError> {
let kd = &mut self.kindata;
kd.analyze_reactions();
kd.calc_sym_constants(None, None, Some(self.T_scaling.clone()));
Ok(())
}
pub fn mean_molar_mass(&mut self) -> Result<(), ReactorError> {
println!("DEBUG mean_molar_mass: Entering function");
println!(
"DEBUG mean_molar_mass: Current molar masses: {:?}",
self.kindata.stecheodata.vec_of_molmasses
);
let mut mean_mass_inv = 0.0;
let molar_masses = self
.kindata
.stecheodata
.vec_of_molmasses
.as_ref()
.ok_or_else(|| ReactorError::MissingData("Molar masses not calculated".to_string()))?;
println!(
"DEBUG mean_molar_mass: Using molar masses: {:?}",
molar_masses
);
if self.M == 0.0 || self.M.is_nan() {
for (i, substance) in self.kindata.substances.iter().enumerate() {
if let Some(conc) = self.boundary_condition.get(substance) {
let mol_mass = molar_masses.get(i).ok_or_else(|| {
ReactorError::IndexOutOfBounds(format!(
"Molar mass index {} out of bounds",
i
))
})?;
mean_mass_inv += conc / mol_mass;
}
}
let mean_mass = 1.0 / mean_mass_inv;
self.M = mean_mass / 1000.0; }
Ok(())
}
pub fn scaling_processing(&mut self) -> Result<(), ReactorError> {
self.scaling.validate()?;
let dT = self.scaling.dT;
let T_scale = self.scaling.T_scale;
let Teta = Expr::Var("Teta".to_owned());
self.T_scaling = Teta.clone() * Expr::Const(T_scale) + Expr::Const(dT);
self.L = self.scaling.L;
Ok(())
}
pub fn peclet_numbers(&mut self) -> Result<(), ReactorError> {
if self.Lambda <= 0.0 {
return Err(ReactorError::InvalidConfiguration(
"Lambda must be positive".to_string(),
));
}
if self.m <= 0.0 {
return Err(ReactorError::InvalidConfiguration(
"Mass flow rate must be positive".to_string(),
));
}
if self.L.is_nan() {
return Err(ReactorError::InvalidConfiguration("missing L".to_string()));
}
if self.m.is_nan() {
return Err(ReactorError::InvalidConfiguration("missing m".to_string()));
}
if self.Cp.is_nan() {
return Err(ReactorError::InvalidConfiguration("missing Cp".to_string()));
}
if self.Lambda.is_nan() {
return Err(ReactorError::InvalidConfiguration(
"missing Lambda".to_string(),
));
}
let Pe_q = (self.L * self.m * self.Cp) / self.Lambda;
self.Pe_q = Pe_q;
Ok(())
}
pub fn ideal_gas_density(&self) -> f64 {
self.M * self.P / (R_G * self.Tm)
}
}