use crate::Kinetics::kinetics_lib_api::KineticData;
use crate::Kinetics::mechfinder_api::ReactionType as RType;
use crate::Kinetics::mechfinder_api::{ReactionData, parse_kinetic_data_vec};
use eframe::egui;
use std::collections::HashMap;
#[derive(Debug)]
pub struct KineticsApp {
kinetic_data: KineticData,
selected_library: String,
selected_reaction_data: Option<ReactionData>,
selected_equation: Option<String>,
_reaction_input: String,
mechanism_input: String,
search_filter: String,
reaction_type: ReactionType,
added_reactions: Vec<ReactionData>,
show_add_reaction_window: bool,
new_reaction_window: NewReactionWindow,
reaction_cache: HashMap<String, ReactionData>,
}
#[derive(Debug)]
struct NewReactionWindow {
reaction_type: crate::Kinetics::mechfinder_api::ReactionType,
equation: String,
arrenius: [String; 3],
low_rate: [String; 3],
high_rate: [String; 3],
eff_input: String,
pressure_data: String,
}
impl Default for NewReactionWindow {
fn default() -> Self {
Self {
reaction_type: crate::Kinetics::mechfinder_api::ReactionType::Elem,
equation: String::new(),
arrenius: [String::new(), String::new(), String::new()],
low_rate: [String::new(), String::new(), String::new()],
high_rate: [String::new(), String::new(), String::new()],
eff_input: String::new(),
pressure_data: String::new(),
}
}
}
#[derive(Default, PartialEq, Clone, Debug)]
enum ReactionType {
#[default]
Mechanism,
Reaction,
}
impl Default for KineticsApp {
fn default() -> Self {
Self {
kinetic_data: KineticData::new(),
selected_library: String::new(),
selected_reaction_data: None,
selected_equation: None,
_reaction_input: String::new(),
mechanism_input: String::new(),
search_filter: String::new(),
reaction_type: ReactionType::default(),
added_reactions: Vec::new(),
show_add_reaction_window: false,
new_reaction_window: NewReactionWindow::default(),
reaction_cache: HashMap::new(),
}
}
}
impl KineticsApp {
pub fn new() -> Self {
let mut app = Self::default();
app.selected_library = "NUIG".to_string();
app.load_library_reactions();
app
}
fn load_library_reactions(&mut self) {
self.kinetic_data.open_json_files(&self.selected_library);
self.kinetic_data.print_all_reactions();
self.selected_reaction_data = None;
self.selected_equation = None;
self.reaction_cache.clear();
}
fn parse_selected_reaction(&mut self, equation: &str) {
if let Some(cached_data) = self.reaction_cache.get(equation) {
self.selected_reaction_data = Some(cached_data.clone());
return;
}
let (_, reaction_value) = self.kinetic_data.search_reaction_by_equation(equation);
if let Ok(reaction_data) = serde_json::from_value::<ReactionData>(reaction_value) {
self.reaction_cache
.insert(equation.to_string(), reaction_data.clone());
self.selected_reaction_data = Some(reaction_data);
}
}
fn show_add_reaction_window(&mut self, ctx: &egui::Context) {
egui::Window::new("Add New Reaction")
.open(&mut self.show_add_reaction_window.clone())
.default_size([400.0, 500.0])
.show(ctx, |ui| {
ui.heading("Create New Reaction");
egui::ComboBox::from_label("Reaction Type")
.selected_text(format!("{:?}", self.new_reaction_window.reaction_type))
.show_ui(ui, |ui| {
ui.selectable_value(
&mut self.new_reaction_window.reaction_type,
RType::Elem,
"Elementary",
);
ui.selectable_value(
&mut self.new_reaction_window.reaction_type,
RType::Falloff,
"Falloff",
);
ui.selectable_value(
&mut self.new_reaction_window.reaction_type,
RType::ThreeBody,
"ThreeBody",
);
ui.selectable_value(
&mut self.new_reaction_window.reaction_type,
RType::Pressure,
"Pressure",
);
});
ui.separator();
ui.horizontal(|ui| {
ui.label("Equation:");
ui.text_edit_singleline(&mut self.new_reaction_window.equation);
});
ui.separator();
match self.new_reaction_window.reaction_type {
crate::Kinetics::mechfinder_api::ReactionType::Elem => {
ui.label("Arrhenius Parameters [A, n, E]:");
ui.horizontal(|ui| {
ui.label("A:");
ui.text_edit_singleline(&mut self.new_reaction_window.arrenius[0]);
});
ui.horizontal(|ui| {
ui.label("n:");
ui.text_edit_singleline(&mut self.new_reaction_window.arrenius[1]);
});
ui.horizontal(|ui| {
ui.label("E:");
ui.text_edit_singleline(&mut self.new_reaction_window.arrenius[2]);
});
}
crate::Kinetics::mechfinder_api::ReactionType::Falloff => {
ui.label("Low Rate [A, n, E]:");
for i in 0..3 {
ui.horizontal(|ui| {
ui.label(format!("Low[{}]:", i));
ui.text_edit_singleline(&mut self.new_reaction_window.low_rate[i]);
});
}
ui.label("High Rate [A, n, E]:");
for i in 0..3 {
ui.horizontal(|ui| {
ui.label(format!("High[{}]:", i));
ui.text_edit_singleline(&mut self.new_reaction_window.high_rate[i]);
});
}
}
crate::Kinetics::mechfinder_api::ReactionType::ThreeBody => {
ui.label("Arrhenius Parameters [A, n, E]:");
for i in 0..3 {
ui.horizontal(|ui| {
ui.label(format!("Arr[{}]:", i));
ui.text_edit_singleline(&mut self.new_reaction_window.arrenius[i]);
});
}
ui.horizontal(|ui| {
ui.label("Efficiencies (JSON):");
ui.text_edit_singleline(&mut self.new_reaction_window.eff_input);
});
}
crate::Kinetics::mechfinder_api::ReactionType::Pressure => {
ui.horizontal(|ui| {
ui.label("Pressure Data (JSON):");
ui.text_edit_multiline(&mut self.new_reaction_window.pressure_data);
});
}
_ => {}
}
ui.separator();
ui.horizontal(|ui| {
if ui.button("Create Reaction").clicked() {
if let Some(reaction) = self.create_reaction_from_window() {
self.added_reactions.push(reaction);
self.show_add_reaction_window = false;
self.new_reaction_window = NewReactionWindow::default();
}
}
if ui.button("Cancel").clicked() {
self.show_add_reaction_window = false;
self.new_reaction_window = NewReactionWindow::default();
}
});
});
}
fn create_reaction_from_window(&self) -> Option<ReactionData> {
if self.new_reaction_window.equation.is_empty() {
return None;
}
match self.new_reaction_window.reaction_type {
crate::Kinetics::mechfinder_api::ReactionType::Elem => {
let arrenius: Result<Vec<f64>, _> = self
.new_reaction_window
.arrenius
.iter()
.map(|s| s.parse::<f64>())
.collect();
if let Ok(arr) = arrenius {
Some(ReactionData::new_elementary(
self.new_reaction_window.equation.clone(),
arr,
None,
))
} else {
None
}
}
crate::Kinetics::mechfinder_api::ReactionType::ThreeBody => {
let arrenius: Result<Vec<f64>, _> = self
.new_reaction_window
.arrenius
.iter()
.map(|s| s.parse::<f64>())
.collect();
let eff: Result<HashMap<String, f64>, _> =
serde_json::from_str(&self.new_reaction_window.eff_input);
if let (Ok(arr), Ok(eff_map)) = (arrenius, eff) {
Some(ReactionData::new_three_body(
self.new_reaction_window.equation.clone(),
arr,
eff_map,
None,
))
} else {
None
}
}
_ => None, }
}
pub fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new("Selecting and adding reactions and mechanisms")
.open(open)
.default_size([1200.0, 800.0])
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.set_width(400.0);
ui.set_min_height(600.0);
ui.heading("List of Reactions");
ui.horizontal(|ui| {
ui.label("Search:");
ui.text_edit_singleline(&mut self.search_filter);
});
ui.separator();
egui::ScrollArea::vertical()
.max_height(1200.0)
.show(ui, |ui| {
for equation in &self.kinetic_data.AllEquations.clone() {
if self.search_filter.is_empty()
|| equation
.to_lowercase()
.contains(&self.search_filter.to_lowercase())
{
let is_selected =
self.selected_equation.as_ref() == Some(equation);
if ui.selectable_label(is_selected, equation).clicked() {
self.selected_equation = Some(equation.clone());
self.parse_selected_reaction(equation);
}
}
}
});
ui.separator();
egui::ComboBox::from_label("Mechanism Source")
.selected_text(&self.selected_library)
.show_ui(ui, |ui| {
for library in &self.kinetic_data.AllLibraries.clone() {
if ui
.selectable_value(
&mut self.selected_library,
library.clone(),
library,
)
.clicked()
{
self.load_library_reactions();
}
}
});
});
ui.separator();
ui.vertical(|ui| {
ui.heading("Chosen Reaction Details");
if let Some(reaction) = &self.selected_reaction_data {
ui.group(|ui| {
ui.set_min_height(150.0);
ui.label(format!("Equation: {}", reaction.eq));
ui.label(format!("Type: {:?}", reaction.reaction_type));
ui.label("Kinetic data:");
ui.label(format!("{:#?}", reaction.data));
});
} else {
ui.group(|ui| {
ui.set_min_height(150.0);
ui.label("Выберите реакцию из списка");
});
}
ui.separator();
ui.horizontal(|ui| {
if ui.button("Saving reaction for calculation").clicked() {
if let Some(reaction) = &self.selected_reaction_data {
self.added_reactions.push(reaction.clone());
println!("Added reaction: {}", reaction.eq);
}
}
if ui.button("Taking all reactions from mechanism").clicked() {
let reaction_values: Vec<serde_json::Value> =
self.kinetic_data.LibKineticData.values().cloned().collect();
let (parsed_reactions, _) = parse_kinetic_data_vec(reaction_values);
self.added_reactions.extend(parsed_reactions);
println!(
"Added {} reactions from mechanism {}",
self.kinetic_data.LibKineticData.len(),
self.selected_library
);
}
});
ui.horizontal(|ui| {
if ui.button("Searching reactions").clicked() {
println!("Searching reactions");
}
if ui.button("Building sub-mechanism").clicked() {
println!("Building sub-mechanism");
}
});
ui.horizontal(|ui| {
if ui.button("Adding sub-mechanism to calculation").clicked() {
println!("Adding sub-mechanism to calculation");
}
});
ui.separator();
ui.heading("Construct sub-mechanism for these substances:");
ui.horizontal(|ui| {
ui.label("Enter substances to search:");
ui.text_edit_singleline(&mut self.mechanism_input);
});
ui.separator();
ui.horizontal(|ui| {
ui.radio_value(
&mut self.reaction_type,
ReactionType::Mechanism,
"Mechanism",
);
ui.radio_value(
&mut self.reaction_type,
ReactionType::Reaction,
"Reaction",
);
});
ui.horizontal(|ui| {
ui.label("New reactions:");
if ui.button("Add new reaction").clicked() {
self.show_add_reaction_window = true;
}
});
if self.show_add_reaction_window {
self.show_add_reaction_window(ctx);
}
});
});
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kinetics_app_new() {
let app = KineticsApp::new();
assert_eq!(app.selected_library, "NUIG");
assert!(app.added_reactions.is_empty());
assert!(app.selected_reaction_data.is_none());
assert!(app.selected_equation.is_none());
}
#[test]
fn test_kinetics_app_default() {
let app = KineticsApp::default();
assert!(app.selected_library.is_empty());
assert!(app.added_reactions.is_empty());
assert!(app.selected_reaction_data.is_none());
assert!(app.selected_equation.is_none());
assert_eq!(app.reaction_type, ReactionType::Mechanism);
}
#[test]
fn test_load_library_reactions() {
let mut app = KineticsApp::default();
app.selected_library = "NUIG".to_string();
app.load_library_reactions();
assert!(!app.kinetic_data.AllLibraries.is_empty());
assert!(!app.kinetic_data.AllEquations.is_empty());
assert!(app.selected_reaction_data.is_none());
assert!(app.selected_equation.is_none());
}
#[test]
fn test_parse_selected_reaction() {
let mut app = KineticsApp::new();
if !app.kinetic_data.AllEquations.is_empty() {
let first_equation = app.kinetic_data.AllEquations[0].clone();
app.parse_selected_reaction(&first_equation);
assert!(app.selected_reaction_data.is_some());
if let Some(reaction) = &app.selected_reaction_data {
assert_eq!(reaction.eq, first_equation);
}
}
}
#[test]
fn test_reaction_type_enum() {
let mechanism = ReactionType::Mechanism;
let reaction = ReactionType::Reaction;
assert_eq!(mechanism, ReactionType::default());
assert_ne!(mechanism, reaction);
}
#[test]
fn test_add_single_reaction() {
let mut app = KineticsApp::new();
if !app.kinetic_data.AllEquations.is_empty() {
let first_equation = app.kinetic_data.AllEquations[0].clone();
app.parse_selected_reaction(&first_equation);
let initial_count = app.added_reactions.len();
if let Some(reaction) = &app.selected_reaction_data {
app.added_reactions.push(reaction.clone());
assert_eq!(app.added_reactions.len(), initial_count + 1);
assert_eq!(app.added_reactions.last().unwrap().eq, first_equation);
}
}
}
#[test]
fn test_add_all_reactions_from_mechanism() {
let mut app = KineticsApp::new();
let reaction_values: Vec<serde_json::Value> =
app.kinetic_data.LibKineticData.values().cloned().collect();
let (parsed_reactions, _) = parse_kinetic_data_vec(reaction_values);
let initial_count = app.added_reactions.len();
app.added_reactions.extend(parsed_reactions.clone());
assert_eq!(
app.added_reactions.len(),
initial_count + parsed_reactions.len()
);
assert!(!app.added_reactions.is_empty());
}
#[test]
fn test_library_switching() {
let mut app = KineticsApp::new();
let initial_library = app.selected_library.clone();
if app.kinetic_data.AllLibraries.len() > 1 {
let new_library = app
.kinetic_data
.AllLibraries
.iter()
.find(|&lib| lib != &initial_library)
.unwrap()
.clone();
app.selected_library = new_library.clone();
app.load_library_reactions();
assert_eq!(app.selected_library, new_library);
assert!(app.selected_reaction_data.is_none());
assert!(app.selected_equation.is_none());
}
}
#[test]
fn test_search_filter_functionality() {
let app = KineticsApp::new();
let search_term = "H2O";
let filtered_equations: Vec<&String> = app
.kinetic_data
.AllEquations
.iter()
.filter(|equation| {
equation
.to_lowercase()
.contains(&search_term.to_lowercase())
})
.collect();
if !filtered_equations.is_empty() {
assert!(
filtered_equations
.iter()
.all(|eq| eq.to_lowercase().contains(&search_term.to_lowercase()))
);
}
}
#[test]
fn test_reaction_data_consistency() {
let mut app = KineticsApp::new();
if !app.kinetic_data.AllEquations.is_empty() {
let equation = app.kinetic_data.AllEquations[0].clone();
app.parse_selected_reaction(&equation);
if let Some(reaction) = &app.selected_reaction_data {
assert!(!reaction.eq.is_empty());
assert!(matches!(
reaction.reaction_type,
crate::Kinetics::mechfinder_api::ReactionType::Elem
| crate::Kinetics::mechfinder_api::ReactionType::Falloff
| crate::Kinetics::mechfinder_api::ReactionType::Pressure
| crate::Kinetics::mechfinder_api::ReactionType::ThreeBody
| crate::Kinetics::mechfinder_api::ReactionType::Empirical
));
}
}
}
#[test]
fn test_input_fields_initialization() {
let app = KineticsApp::default();
assert!(app._reaction_input.is_empty());
assert!(app.mechanism_input.is_empty());
assert!(app.search_filter.is_empty());
}
#[test]
fn test_added_reactions_uniqueness() {
let mut app = KineticsApp::new();
if !app.kinetic_data.AllEquations.is_empty() {
let equation = app.kinetic_data.AllEquations[0].clone();
app.parse_selected_reaction(&equation);
if let Some(reaction) = &app.selected_reaction_data {
app.added_reactions.push(reaction.clone());
app.added_reactions.push(reaction.clone());
assert_eq!(app.added_reactions.len(), 2);
assert_eq!(app.added_reactions[0].eq, app.added_reactions[1].eq);
}
}
}
#[test]
fn test_new_reaction_window_default() {
let window = NewReactionWindow::default();
assert_eq!(
window.reaction_type,
crate::Kinetics::mechfinder_api::ReactionType::Elem
);
assert!(window.equation.is_empty());
assert_eq!(
window.arrenius,
[String::new(), String::new(), String::new()]
);
assert_eq!(
window.low_rate,
[String::new(), String::new(), String::new()]
);
assert_eq!(
window.high_rate,
[String::new(), String::new(), String::new()]
);
assert!(window.eff_input.is_empty());
assert!(window.pressure_data.is_empty());
}
#[test]
fn test_create_elementary_reaction_from_window() {
let mut app = KineticsApp::default();
app.new_reaction_window.equation = "H2 + O <=> H + OH".to_string();
app.new_reaction_window.reaction_type = crate::Kinetics::mechfinder_api::ReactionType::Elem;
app.new_reaction_window.arrenius = [
"1.0e13".to_string(),
"0.0".to_string(),
"15000.0".to_string(),
];
let reaction = app.create_reaction_from_window();
assert!(reaction.is_some());
let reaction = reaction.unwrap();
assert_eq!(reaction.eq, "H2 + O <=> H + OH");
assert_eq!(
reaction.reaction_type,
crate::Kinetics::mechfinder_api::ReactionType::Elem
);
}
#[test]
fn test_create_threebody_reaction_from_window() {
let mut app = KineticsApp::default();
app.new_reaction_window.equation = "H2 + M <=> H + H + M".to_string();
app.new_reaction_window.reaction_type =
crate::Kinetics::mechfinder_api::ReactionType::ThreeBody;
app.new_reaction_window.arrenius = [
"1.0e14".to_string(),
"-1.0".to_string(),
"104000.0".to_string(),
];
app.new_reaction_window.eff_input = r#"{"H2": 2.5, "H2O": 12.0}"#.to_string();
let reaction = app.create_reaction_from_window();
assert!(reaction.is_some());
let reaction = reaction.unwrap();
assert_eq!(reaction.eq, "H2 + M <=> H + H + M");
assert_eq!(
reaction.reaction_type,
crate::Kinetics::mechfinder_api::ReactionType::ThreeBody
);
}
#[test]
fn test_create_reaction_empty_equation() {
let mut app = KineticsApp::default();
app.new_reaction_window.equation = "".to_string();
app.new_reaction_window.arrenius =
["1.0".to_string(), "2.0".to_string(), "3.0".to_string()];
let reaction = app.create_reaction_from_window();
assert!(reaction.is_none());
}
#[test]
fn test_create_reaction_invalid_arrenius() {
let mut app = KineticsApp::default();
app.new_reaction_window.equation = "A + B <=> C".to_string();
app.new_reaction_window.arrenius =
["invalid".to_string(), "2.0".to_string(), "3.0".to_string()];
let reaction = app.create_reaction_from_window();
assert!(reaction.is_none());
}
#[test]
fn test_create_threebody_invalid_eff() {
let mut app = KineticsApp::default();
app.new_reaction_window.equation = "H2 + M <=> H + H + M".to_string();
app.new_reaction_window.reaction_type =
crate::Kinetics::mechfinder_api::ReactionType::ThreeBody;
app.new_reaction_window.arrenius =
["1.0".to_string(), "2.0".to_string(), "3.0".to_string()];
app.new_reaction_window.eff_input = "invalid json".to_string();
let reaction = app.create_reaction_from_window();
assert!(reaction.is_none());
}
#[test]
fn test_show_add_reaction_window_toggle() {
let mut app = KineticsApp::default();
assert!(!app.show_add_reaction_window);
app.show_add_reaction_window = true;
assert!(app.show_add_reaction_window);
}
#[test]
fn test_new_reaction_window_reset() {
let mut window = NewReactionWindow::default();
window.equation = "test".to_string();
window.arrenius[0] = "123".to_string();
window.eff_input = "test".to_string();
window = NewReactionWindow::default();
assert!(window.equation.is_empty());
assert_eq!(window.arrenius[0], String::new());
assert!(window.eff_input.is_empty());
}
}