use crate::Kinetics::User_reactions::KinData;
use crate::Kinetics::mechfinder_api::ReactionData;
use crate::ReactorsBVP::reactor_BVP_utils::{
BoundsConfig, ScalingConfig, ToleranceConfig, create_bounds_map, create_tolerance_map,
};
use crate::ReactorsBVP::solver_backend::ReactorBvpSolverConfig;
use RustedSciThe::numerical::BVP_Damp::NR_Damp_solver_damped::{
DampedSolverOptions, NRBVP, SolverParams,
};
use RustedSciThe::numerical::BVP_sci::BVP_sci_symb::BVPwrap as BVPsci;
use RustedSciThe::symbolic::symbolic_engine::Expr;
use log::{info, warn};
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)]
pub enum ReactorError {
MissingData(String),
InvalidConfiguration(String),
InvalidNumericValue(String),
CalculationError(String),
ParseError(String),
IndexOutOfBounds(String),
}
impl fmt::Display for ReactorError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ReactorError::MissingData(msg) => write!(f, "Missing data: {}", msg),
ReactorError::InvalidConfiguration(msg) => write!(f, "Invalid configuration: {}", msg),
ReactorError::InvalidNumericValue(msg) => write!(f, "Invalid numeric value: {}", msg),
ReactorError::CalculationError(msg) => write!(f, "Calculation error: {}", msg),
ReactorError::ParseError(msg) => write!(f, "Parse error: {}", msg),
ReactorError::IndexOutOfBounds(msg) => write!(f, "Index out of bounds: {}", msg),
}
}
}
impl std::error::Error for ReactorError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolicRhsAssemblyPolicy {
Auto,
Sequential,
Parallel,
}
#[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 Diffusion: HashMap<String, f64>,
pub m: f64,
pub scaling: ScalingConfig,
pub L: f64,
pub T_scaling: Expr,
pub M: f64,
pub D_ro_map: HashMap<String, f64>,
pub Pe_q: f64,
pub Pe_D: Vec<f64>,
pub map_eq_rate: HashMap<String, Expr>,
pub map_of_equations: HashMap<String, (String, Expr)>,
pub heat_release: Expr,
pub symbolic_rhs_assembly_policy: SymbolicRhsAssemblyPolicy,
pub solver: BVPSolver,
}
#[derive(Debug, Clone)]
pub struct BVPSolver {
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 BVPSolver {
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(),
}
}
}
pub(crate) fn validate_bvp_solution_matrix(solution: &DMatrix<f64>) -> Result<(), ReactorError> {
if solution.nrows() == 0 || solution.ncols() == 0 {
return Err(ReactorError::CalculationError(
"BVP solver returned an empty solution matrix".to_string(),
));
}
if let Some((idx, value)) = solution
.iter()
.copied()
.enumerate()
.find(|(_, value)| !value.is_finite())
{
return Err(ReactorError::InvalidNumericValue(format!(
"BVP solver returned non-finite value {} at flat index {}",
value, idx
)));
}
Ok(())
}
#[derive(Debug)]
pub(crate) struct NrbvpHandoffConfig {
pub initial_guess: DMatrix<f64>,
pub t0: f64,
pub t_end: f64,
pub n_steps: usize,
pub solver_backend_config: ReactorBvpSolverConfig,
pub scheme: String,
pub strategy: String,
pub strategy_params: Option<SolverParams>,
pub linear_sys_method: Option<String>,
pub method: String,
pub abs_tolerance: f64,
pub rel_tolerance: Option<HashMap<String, f64>>,
pub max_iterations: usize,
pub bounds: Option<HashMap<String, (f64, f64)>>,
pub loglevel: Option<String>,
pub dont_save_log: bool,
}
impl NrbvpHandoffConfig {
pub(crate) fn new(
initial_guess: DMatrix<f64>,
t0: f64,
t_end: f64,
n_steps: usize,
scheme: String,
strategy: String,
strategy_params: Option<SolverParams>,
linear_sys_method: Option<String>,
method: String,
abs_tolerance: f64,
rel_tolerance: Option<HashMap<String, f64>>,
max_iterations: usize,
bounds: Option<HashMap<String, (f64, f64)>>,
loglevel: Option<String>,
dont_save_log: bool,
) -> Self {
Self {
initial_guess,
t0,
t_end,
n_steps,
solver_backend_config: ReactorBvpSolverConfig::default_lambdify(),
scheme,
strategy,
strategy_params,
linear_sys_method,
method,
abs_tolerance,
rel_tolerance,
max_iterations,
bounds,
loglevel,
dont_save_log,
}
}
pub(crate) fn with_solver_backend_config(
mut self,
solver_backend_config: ReactorBvpSolverConfig,
) -> Self {
self.solver_backend_config = solver_backend_config;
self
}
}
fn build_damped_options(config: &NrbvpHandoffConfig) -> Result<DampedSolverOptions, ReactorError> {
let mut options = config
.solver_backend_config
.to_rusted_options()
.map_err(ReactorError::InvalidConfiguration)?;
options = options
.with_scheme_name(config.scheme.clone())
.with_strategy_params(config.strategy_params.clone())
.with_abs_tolerance(config.abs_tolerance)
.with_max_iterations(config.max_iterations)
.with_loglevel(config.loglevel.clone());
options.strategy = config.strategy.clone();
options.linear_sys_method = config.linear_sys_method.clone();
options.method = config.method.clone();
if let Some(rel_tolerance) = config.rel_tolerance.clone() {
options = options.with_rel_tolerance(rel_tolerance);
}
if let Some(bounds) = config.bounds.clone() {
options = options.with_bounds(bounds);
}
Ok(options)
}
impl BVPSolver {
pub(crate) fn backend_boundary_conditions(&self) -> HashMap<String, Vec<(usize, f64)>> {
let mut backend_bc = HashMap::with_capacity(self.BorderConditions.len());
for (name, &(boundary_index, value)) in &self.BorderConditions {
backend_bc.insert(name.clone(), vec![(boundary_index, value)]);
}
backend_bc
}
pub(crate) fn build_nrbvp_backend(
&self,
config: NrbvpHandoffConfig,
) -> Result<NRBVP, ReactorError> {
let options = build_damped_options(&config)?;
let mut backend = NRBVP::new_with_options(
self.eq_system.clone(),
config.initial_guess,
self.unknowns.clone(),
self.arg_name.clone(),
self.backend_boundary_conditions(),
config.t0,
config.t_end,
config.n_steps,
options,
);
backend.dont_save_log(config.dont_save_log);
Ok(backend)
}
pub fn solve_NRBVP(
&mut self,
initial_guess: DMatrix<f64>,
n_steps: usize,
scheme: String,
strategy: String,
strategy_params: Option<SolverParams>,
linear_sys_method: Option<String>,
method: String,
abs_tolerance: f64,
rel_tolerance: Option<HashMap<String, f64>>,
max_iterations: usize,
Bounds: Option<HashMap<String, (f64, f64)>>,
loglevel: Option<String>,
) -> Result<(), ReactorError> {
self.solve_NRBVP_impl(
initial_guess,
n_steps,
scheme,
strategy,
strategy_params,
linear_sys_method,
method,
abs_tolerance,
rel_tolerance,
max_iterations,
Bounds,
loglevel,
)
}
pub fn solve_NRBVP_with_tolerance_config(
&mut self,
initial_guess: DMatrix<f64>,
n_steps: usize,
scheme: String,
strategy: String,
strategy_params: Option<SolverParams>,
linear_sys_method: Option<String>,
method: String,
abs_tolerance: f64,
tolerance_config: ToleranceConfig,
substances: &[String],
max_iterations: usize,
Bounds: Option<HashMap<String, (f64, f64)>>,
loglevel: Option<String>,
) -> Result<(), ReactorError> {
let rel_tolerance = Some(tolerance_config.to_full_tolerance_map(substances));
self.solve_NRBVP_impl(
initial_guess,
n_steps,
scheme,
strategy,
strategy_params,
linear_sys_method,
method,
abs_tolerance,
rel_tolerance,
max_iterations,
Bounds,
loglevel,
)
}
pub fn solve_NRBVP_with_configs(
&mut self,
initial_guess: DMatrix<f64>,
n_steps: usize,
scheme: String,
strategy: String,
strategy_params: Option<SolverParams>,
linear_sys_method: Option<String>,
method: String,
abs_tolerance: f64,
tolerance_config: ToleranceConfig,
bounds_config: BoundsConfig,
substances: &[String],
max_iterations: usize,
loglevel: Option<String>,
) -> Result<(), ReactorError> {
let rel_tolerance = Some(tolerance_config.to_full_tolerance_map(substances));
let bounds = Some(bounds_config.to_full_bounds_map(substances));
self.solve_NRBVP_impl(
initial_guess,
n_steps,
scheme,
strategy,
strategy_params,
linear_sys_method,
method,
abs_tolerance,
rel_tolerance,
max_iterations,
bounds,
loglevel,
)
}
fn solve_NRBVP_impl(
&mut self,
initial_guess: DMatrix<f64>,
n_steps: usize,
scheme: String,
strategy: String,
strategy_params: Option<SolverParams>,
linear_sys_method: Option<String>,
method: String,
abs_tolerance: f64,
rel_tolerance: Option<HashMap<String, f64>>,
max_iterations: usize,
Bounds: Option<HashMap<String, (f64, f64)>>,
loglevel: Option<String>,
) -> Result<(), ReactorError> {
info!("starting solver!");
let mut bvp = self.build_nrbvp_backend(NrbvpHandoffConfig::new(
initial_guess,
self.x_range.0,
self.x_range.1,
n_steps,
scheme,
strategy,
strategy_params,
linear_sys_method,
method,
abs_tolerance,
rel_tolerance,
max_iterations,
Bounds,
loglevel,
false,
))?;
let solve_result = bvp
.try_solve()
.map_err(|err| ReactorError::CalculationError(format!("{err:?}")))?;
let converged = solve_result.is_some();
if !converged {
warn!("BVP solver did not converge to a solution");
}
let solution = bvp.get_result().ok_or_else(|| {
ReactorError::CalculationError(
"BVP solver finished without producing a solution matrix".to_string(),
)
})?;
validate_bvp_solution_matrix(&solution)?;
if bvp.x_mesh.is_empty() {
return Err(ReactorError::CalculationError(
"BVP solver finished without producing an x mesh".to_string(),
));
}
if bvp.x_mesh.iter().any(|value| !value.is_finite()) {
return Err(ReactorError::InvalidNumericValue(
"BVP solver x mesh contains non-finite values".to_string(),
));
}
self.solution = Some(solution);
self.x_mesh = Some(bvp.x_mesh);
if converged {
Ok(())
} else {
Err(ReactorError::CalculationError(
"BVP solver did not converge to a solution".to_string(),
))
}
}
pub fn solve_BVPsci(
&mut self,
initial_guess: DMatrix<f64>,
n_steps: usize,
max_nodes: usize,
tol: f64,
) {
let BC = self.backend_boundary_conditions();
let mut bvp = BVPsci::new(
None,
Some(0.0 as f64),
Some(1.0 as f64),
Some(n_steps),
self.eq_system.clone(),
self.unknowns.clone(),
vec![],
None,
BC,
"x".to_string(),
tol,
max_nodes,
initial_guess,
);
bvp.solve();
if let Some(solution) = bvp.get_result() {
self.solution = Some(solution);
self.x_mesh = Some(bvp.mesh());
}
}
pub fn get_solution(&self) -> Option<&DMatrix<f64>> {
self.solution.as_ref()
}
pub fn debug_solution(&self) {
if let Some(solution) = &self.solution {
info!("\n=== SOLUTION DEBUG ===");
info!(
"Solution matrix shape: {} x {}",
solution.nrows(),
solution.ncols()
);
info!("Unknowns: {:?}", self.unknowns);
for (i, var_name) in self.unknowns.iter().enumerate() {
if i < solution.ncols() {
let col = solution.column(i);
info!(
"{}: [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]
);
}
}
info!("=== 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,
Diffusion: HashMap::new(),
m: 0.0,
scaling: ScalingConfig::default(),
L: 1.0,
T_scaling: Expr::Const(0.0),
M: 0.0,
D_ro_map: HashMap::new(),
Pe_q: 0.0,
Pe_D: Vec::new(),
map_eq_rate: HashMap::new(),
map_of_equations: HashMap::new(),
heat_release: Expr::Const(0.0),
symbolic_rhs_assembly_policy: SymbolicRhsAssemblyPolicy::Auto,
solver: BVPSolver::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_symbolic_rhs_assembly_policy(&mut self, policy: SymbolicRhsAssemblyPolicy) {
self.symbolic_rhs_assembly_policy = policy;
}
pub fn with_symbolic_rhs_assembly_policy(mut self, policy: SymbolicRhsAssemblyPolicy) -> Self {
self.symbolic_rhs_assembly_policy = policy;
self
}
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 diffusion_coefficients(&self) -> &HashMap<String, f64> {
&self.Diffusion
}
pub fn boundary_conditions(&self) -> &HashMap<String, f64> {
&self.boundary_condition
}
pub fn transport_cache(&self) -> &HashMap<String, f64> {
&self.D_ro_map
}
pub fn mass_peclet_numbers(&self) -> &[f64] {
&self.Pe_D
}
pub fn thermal_peclet_number(&self) -> f64 {
self.Pe_q
}
pub fn temperature_shift(&self) -> f64 {
self.scaling.dT
}
pub fn characteristic_length(&self) -> f64 {
self.scaling.L
}
pub fn temperature_scale(&self) -> f64 {
self.scaling.T_scale
}
pub fn scaling_config(&self) -> &ScalingConfig {
&self.scaling
}
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.Diffusion = Diffusion;
self.m = m;
self.scaling = scaling;
}
pub fn setup_bvp(&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_bvp_equations()?;
info!("BVP equations created");
self.set_solver_BC()?;
info!("Boundary conditions created!");
self.check_before_solution()?;
info!("BVP setup completed!");
Ok(())
}
pub fn set_transport_properties(
&mut self,
lambda: f64,
cp: f64,
diffusion: HashMap<String, f64>,
) {
self.Lambda = lambda;
self.Cp = cp;
self.Diffusion = diffusion;
}
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 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_finite() {
return Err(ReactorError::MissingData(format!(
"Missing Arrhenius parameter 'A' in input hashmap at index {}",
idx
)));
}
if !n.is_finite() {
return Err(ReactorError::MissingData(format!(
"Missing Arrhenius parameter 'n' in input hashmap at index {}",
idx
)));
}
if !E.is_finite() {
return Err(ReactorError::MissingData(format!(
"Missing Arrhenius parameter 'E' in input hashmap at index {}",
idx
)));
}
if !Q.is_finite() {
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);
Q_vec.push(Q);
elementary_reaction_vec.push(reactdata);
}
let mut kindata = KinData::new();
kindata
.set_reaction_data_directly(elementary_reaction_vec, None)
.map_err(|err| ReactorError::CalculationError(err.to_string()))?;
self.kindata = kindata;
self.thermal_effects = Q_vec;
Ok(())
}
pub fn check_task(&self) -> Result<(), ReactorError> {
let ensure_finite_positive = |name: &str, value: f64| -> Result<(), ReactorError> {
if !value.is_finite() {
return Err(ReactorError::InvalidNumericValue(format!(
"{} must be finite, got {}",
name, value
)));
}
if value <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"{} must be positive, got {}",
name, value
)));
}
Ok(())
};
ensure_finite_positive("P", self.P)?;
ensure_finite_positive("Tm", self.Tm)?;
ensure_finite_positive("Cp", self.Cp)?;
ensure_finite_positive("Lambda", self.Lambda)?;
ensure_finite_positive("m", self.m)?;
ensure_finite_positive("M", self.M)?;
if self.Diffusion.len() != self.kindata.substances.len() {
return Err(ReactorError::InvalidConfiguration(
"Diffusion entries must match number of substances".to_string(),
));
}
for substance in &self.kindata.substances {
if !self.Diffusion.contains_key(substance) {
return Err(ReactorError::MissingData(format!(
"Missing diffusion coefficient for {}",
substance
)));
}
let diffusion = *self.Diffusion.get(substance).ok_or_else(|| {
ReactorError::MissingData(format!(
"Missing diffusion coefficient for {}",
substance
))
})?;
if !diffusion.is_finite() || diffusion <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Diffusion coefficient for {} must be finite and positive, got {}",
substance, diffusion
)));
}
}
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(),
));
}
for (idx, effect) in self.thermal_effects.iter().enumerate() {
if !effect.is_finite() {
return Err(ReactorError::InvalidNumericValue(format!(
"Thermal effect at index {} must be finite, got {}",
idx, effect
)));
}
}
self.scaling.validate()?;
let temperature = self.boundary_condition.get("T").ok_or_else(|| {
ReactorError::MissingData("Missing T in boundary conditions".to_string())
})?;
if !temperature.is_finite() || *temperature <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Boundary temperature must be finite and positive, got {}",
temperature
)));
}
let mut boundary_fraction_sum = 0.0;
for substance in &self.kindata.substances {
let fraction = *self.boundary_condition.get(substance).ok_or_else(|| {
ReactorError::MissingData(format!("Missing boundary condition for {}", substance))
})?;
if !fraction.is_finite() || fraction < 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Boundary fraction for {} must be finite and non-negative, got {}",
substance, fraction
)));
}
boundary_fraction_sum += fraction;
}
if !boundary_fraction_sum.is_finite() || boundary_fraction_sum <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Boundary fractions must sum to a positive finite value, got {}",
boundary_fraction_sum
)));
}
if (boundary_fraction_sum - 1.0).abs() > 1e-6 {
return Err(ReactorError::InvalidConfiguration(format!(
"Boundary fractions must be normalized to 1.0, got {}",
boundary_fraction_sum
)));
}
Ok(())
}
pub fn check_before_solution(&self) -> Result<(), ReactorError> {
let n_substances = self.kindata.substances.len();
let expected_len = 2 * 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.Pe_D.len() != n_substances {
return Err(ReactorError::InvalidConfiguration(format!(
"Pe_D length {} != substances {}",
self.Pe_D.len(),
n_substances
)));
}
if !self.M.is_finite() || self.M <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"M must be finite and positive, got {}",
self.M
)));
}
if !self.Pe_q.is_finite() || self.Pe_q <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Pe_q must be finite and positive, got {}",
self.Pe_q
)));
}
for (idx, pe_d) in self.Pe_D.iter().enumerate() {
if !pe_d.is_finite() || *pe_d <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Pe_D[{}] must be finite and positive, got {}",
idx, pe_d
)));
}
}
Ok(())
}
pub fn kinetic_processing(&mut self) -> Result<(), ReactorError> {
let kd = &mut self.kindata;
kd.analyze_reactions()
.map_err(|err| ReactorError::CalculationError(err.to_string()))?;
kd.calc_sym_constants(None, None, Some(self.T_scaling.clone()))
.map_err(|err| ReactorError::CalculationError(err.to_string()))?;
Ok(())
}
pub fn mean_molar_mass(&mut self) -> Result<(), ReactorError> {
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()))?;
if !self.M.is_finite() || self.M <= 0.0 {
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
))
})?;
if !conc.is_finite() || *conc < 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Boundary fraction for {} must be finite and non-negative, got {}",
substance, conc
)));
}
if !mol_mass.is_finite() || *mol_mass <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Molar mass for {} must be finite and positive, got {}",
substance, mol_mass
)));
}
mean_mass_inv += conc / mol_mass;
}
}
if !mean_mass_inv.is_finite() || mean_mass_inv <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Mean molar mass denominator must be finite and positive, got {}",
mean_mass_inv
)));
}
let mean_mass = 1.0 / mean_mass_inv;
if !mean_mass.is_finite() || mean_mass <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Mean molar mass must be finite and positive, got {}",
mean_mass
)));
}
self.M = mean_mass / 1000.0; }
Ok(())
}
pub fn transport_coefficients(&mut self) -> Result<(), ReactorError> {
if !self.P.is_finite() || self.P <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Pressure must be finite and positive, got {}",
self.P
)));
}
if !self.Tm.is_finite() || self.Tm <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Temperature must be finite and positive, got {}",
self.Tm
)));
}
if !self.M.is_finite() || self.M <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Mean molar mass must be finite and positive, got {}",
self.M
)));
}
let ro0 = self.M * self.P / (R_G * 298.15);
if !ro0.is_finite() || ro0 <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Reference density must be finite and positive, got {}",
ro0
)));
}
let mut D_ro_map = HashMap::new();
for subs in self.kindata.substances.iter() {
if let Some(D_i) = self.Diffusion.get(subs) {
if !D_i.is_finite() || *D_i <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Diffusion coefficient for {} must be finite and positive, got {}",
subs, D_i
)));
}
let D_ro = D_i * ro0 * (self.Tm / 298.15).powf(0.5);
if !D_ro.is_finite() || D_ro <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"D*ro for {} must be finite and positive, got {}",
subs, D_ro
)));
}
D_ro_map.insert(subs.clone(), D_ro);
}
}
self.D_ro_map = D_ro_map;
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.is_finite() || self.Lambda <= 0.0 {
return Err(ReactorError::InvalidConfiguration(
"Lambda must be positive".to_string(),
));
}
if !self.m.is_finite() || self.m <= 0.0 {
return Err(ReactorError::InvalidConfiguration(
"Mass flow rate must be positive".to_string(),
));
}
if !self.L.is_finite() || !self.Cp.is_finite() {
return Err(ReactorError::InvalidNumericValue(
"Peclet inputs must be finite".to_string(),
));
}
let Pe_q = (self.L * self.m * self.Cp) / self.Lambda;
if !Pe_q.is_finite() || Pe_q <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Pe_q must be finite and positive, got {}",
Pe_q
)));
}
let mut Pe_D = Vec::new();
self.transport_coefficients()?;
for subs in self.kindata.substances.iter() {
let ro_D_i = self.D_ro_map.get(subs).ok_or_else(|| {
ReactorError::MissingData(format!(
"Transport coefficient for substance '{}' not found",
subs
))
})?;
if *ro_D_i <= 0.0 {
return Err(ReactorError::InvalidConfiguration(format!(
"Transport coefficient for '{}' must be positive",
subs
)));
}
let Pe_D_i = (self.m * self.L) / ro_D_i;
if !Pe_D_i.is_finite() || Pe_D_i <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Pe_D for '{}' must be finite and positive, got {}",
subs, Pe_D_i
)));
}
Pe_D.push(Pe_D_i);
}
self.Pe_q = Pe_q;
self.Pe_D = Pe_D;
Ok(())
}
pub fn ideal_gas_density(&self) -> Result<f64, ReactorError> {
if !self.M.is_finite() || self.M <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Mean molar mass must be finite and positive, got {}",
self.M
)));
}
if !self.P.is_finite() || self.P <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Pressure must be finite and positive, got {}",
self.P
)));
}
if !self.Tm.is_finite() || self.Tm <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Temperature must be finite and positive, got {}",
self.Tm
)));
}
let density = self.M * self.P / (R_G * self.Tm);
if !density.is_finite() || density <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Ideal gas density must be finite and positive, got {}",
density
)));
}
Ok(density)
}
pub fn Le_number(&self) -> Result<f64, ReactorError> {
if self.D_ro_map.is_empty() {
return Err(ReactorError::MissingData(
"Cannot compute Lewis number without diffusion coefficients".to_string(),
));
}
if !self.Lambda.is_finite() || self.Lambda <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Thermal conductivity must be finite and positive, got {}",
self.Lambda
)));
}
if !self.Cp.is_finite() || self.Cp <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Heat capacity must be finite and positive, got {}",
self.Cp
)));
}
let mut le_sum = 0.0;
let mut le_count = 0usize;
for diffusion in self.D_ro_map.values() {
if !diffusion.is_finite() || *diffusion <= 0.0 {
return Err(ReactorError::InvalidNumericValue(format!(
"Diffusion coefficient must be finite and positive, got {}",
diffusion
)));
}
le_sum += self.Lambda / (self.Cp * diffusion);
le_count += 1;
}
Ok(le_sum / le_count as f64)
}
}