use eframe::egui;
use nalgebra::{DMatrix, DVector};
use crate::Thermodynamics::DBhandlers::thermo_api::{
EnergyUnit, ThermoCalculator, create_thermal_by_name,
};
use crate::Thermodynamics::thermo_lib_api::ThermoData;
use crate::gui::NIST_gui::NISTApp;
use crate::gui::gui_plot::PlotWindow;
#[derive(Debug)]
pub struct ThermochemistryApp {
thermo_data: ThermoData,
substance_input: String,
selected_library: String,
temperature: String,
pressure: String,
search_results: String,
available_substances: Vec<String>,
selected_substance: String,
calculated_cp: Option<f64>,
calculated_dh: Option<f64>,
calculated_ds: Option<f64>,
energy_unit: EnergyUnit,
show_plots_window: bool,
t0: String,
tend: String,
plot_window: Option<PlotWindow>,
show_nist_window: bool,
nist_app: Option<NISTApp>,
}
impl Default for ThermochemistryApp {
fn default() -> Self {
let thermo_data = ThermoData::new();
let selected_library = if !thermo_data.thermo_libs.is_empty() {
thermo_data.thermo_libs[0].clone()
} else {
"NASA_gas".to_string()
};
let available_substances = thermo_data
.LibThermoData
.get(&selected_library)
.map(|lib_data| lib_data.keys().cloned().collect())
.unwrap_or_default();
Self {
thermo_data,
substance_input: String::new(),
selected_library,
temperature: "298.15".to_string(),
pressure: "101325.0".to_string(),
search_results: "No search performed yet".to_string(),
available_substances,
selected_substance: String::new(),
calculated_cp: None,
calculated_dh: None,
calculated_ds: None,
energy_unit: EnergyUnit::J,
show_plots_window: false,
t0: "298.15".to_string(),
tend: "1000.0".to_string(),
plot_window: None,
show_nist_window: false,
nist_app: None,
}
}
}
impl ThermochemistryApp {
pub fn new() -> Self {
Self::default()
}
fn update_substances_for_library(&mut self) {
self.available_substances = self
.thermo_data
.LibThermoData
.get(&self.selected_library)
.map(|lib_data| lib_data.keys().cloned().collect())
.unwrap_or_default();
}
fn search_substance_by_name(&mut self, substance_name: &str) {
self.selected_substance = substance_name.to_string();
if let Some(lib_data) = self.thermo_data.LibThermoData.get(&self.selected_library) {
if let Some(substance_data) = lib_data.get(substance_name) {
self.search_results = format!(
"Found '{}' in library '{}':\n\n{}",
substance_name,
self.selected_library,
serde_json::to_string_pretty(substance_data)
.unwrap_or("Error formatting data".to_string())
);
} else {
self.search_results = format!(
"Substance '{}' not found in library '{}'",
substance_name, self.selected_library
);
}
} else {
self.search_results = format!("Library '{}' not found", self.selected_library);
}
}
#[allow(dead_code)]
fn search_substance(&mut self) {
if self.substance_input.is_empty() {
self.search_results = "Please enter a substance name".to_string();
return;
}
if let Some(lib_data) = self.thermo_data.LibThermoData.get(&self.selected_library) {
if let Some(substance_data) = lib_data.get(&self.substance_input) {
self.search_results = format!(
"Found '{}' in library '{}':\n\n{}",
self.substance_input,
self.selected_library,
serde_json::to_string_pretty(substance_data)
.unwrap_or("Error formatting data".to_string())
);
} else {
self.search_results = format!(
"Substance '{}' not found in library '{}'",
self.substance_input, self.selected_library
);
}
} else {
self.search_results = format!("Library '{}' not found", self.selected_library);
}
}
fn calculate_properties(&mut self) {
let temp_result = self.temperature.parse::<f64>();
let pres_result = self.pressure.parse::<f64>();
let substance_name = if !self.selected_substance.is_empty() {
&self.selected_substance
} else {
&self.substance_input
};
if substance_name.is_empty() {
self.search_results = "Please select a substance first".to_string();
return;
}
match (temp_result, pres_result) {
(Ok(t), Ok(_p)) => {
if let Some(lib_data) = self.thermo_data.LibThermoData.get(&self.selected_library) {
if let Some(substance_data) = lib_data.get(substance_name) {
match self.perform_thermo_calculation(substance_data, t) {
Ok((cp, dh, ds)) => {
self.calculated_cp = Some(cp);
self.calculated_dh = Some(dh);
self.calculated_ds = Some(ds);
let (cp_unit, dh_unit, ds_unit) = match self.energy_unit {
EnergyUnit::J => ("J/mol·K", "kJ/mol", "J/mol·K"),
EnergyUnit::Cal => ("cal/mol·K", "kcal/mol", "cal/mol·K"),
};
self.search_results = format!(
"Thermodynamic properties for '{}' at T = {} K:\n\nCp = {:.3} {}\ndH = {:.3} {}\ndS = {:.3} {}",
substance_name,
t,
cp,
cp_unit,
dh / 1000.0,
dh_unit,
ds,
ds_unit
);
}
Err(e) => {
self.search_results = format!("Calculation error: {}", e);
}
}
} else {
self.search_results =
format!("Substance '{}' not found for calculations", substance_name);
}
} else {
self.search_results = format!(
"Library '{}' not available for calculations",
self.selected_library
);
}
}
_ => {
self.search_results = "Invalid temperature or pressure values".to_string();
}
}
}
fn perform_thermo_calculation(
&self,
substance_data: &serde_json::Value,
temperature: f64,
) -> Result<(f64, f64, f64), String> {
let mut thermoenum = create_thermal_by_name(&self.selected_library);
thermoenum
.newinstance()
.map_err(|e| format!("Failed to create instance: {}", e))?;
thermoenum
.set_unit(Some(self.energy_unit.clone()))
.map_err(|e| format!("Failed to set unit: {}", e))?;
thermoenum
.from_serde(substance_data.clone())
.map_err(|e| format!("Failed to load data: {}", e))?;
thermoenum
.extract_model_coefficients(temperature)
.map_err(|e| format!("Failed to extract coefficients: {}", e))?;
thermoenum
.calculate_Cp_dH_dS(temperature)
.map_err(|e| format!("Failed to calculate properties: {}", e))?;
let cp = thermoenum
.get_Cp()
.map_err(|e| format!("Failed to get Cp: {}", e))?;
let dh = thermoenum
.get_dh()
.map_err(|e| format!("Failed to get dH: {}", e))?;
let ds = thermoenum
.get_ds()
.map_err(|e| format!("Failed to get dS: {}", e))?;
Ok((cp, dh, ds))
}
fn calculate_temperature_range(
&self,
substance_data: &serde_json::Value,
t0: f64,
tend: f64,
n_points: usize,
) -> Result<Vec<(f64, f64, f64, f64)>, String> {
let mut results = Vec::new();
let dt = (tend - t0) / (n_points - 1) as f64;
let mut thermoenum = create_thermal_by_name(&self.selected_library);
thermoenum
.newinstance()
.map_err(|e| format!("Failed to create instance: {}", e))?;
thermoenum
.set_unit(Some(self.energy_unit.clone()))
.map_err(|e| format!("Failed to set unit: {}", e))?;
thermoenum
.from_serde(substance_data.clone())
.map_err(|e| format!("Failed to load data: {}", e))?;
for i in 0..n_points {
let t = t0 + i as f64 * dt;
thermoenum
.extract_model_coefficients(t)
.map_err(|e| format!("Failed to extract coefficients: {}", e))?;
thermoenum
.calculate_Cp_dH_dS(t)
.map_err(|e| format!("Failed to calculate properties: {}", e))?;
let cp = thermoenum
.get_Cp()
.map_err(|e| format!("Failed to get Cp: {}", e))?;
let dh = thermoenum
.get_dh()
.map_err(|e| format!("Failed to get dH: {}", e))?;
let ds = thermoenum
.get_ds()
.map_err(|e| format!("Failed to get dS: {}", e))?;
results.push((t, cp, dh, ds));
}
Ok(results)
}
fn show_plots_window(&mut self, ctx: &egui::Context) {
egui::Window::new("Temperature Range Plots")
.open(&mut self.show_plots_window.clone())
.default_size([800.0, 600.0])
.show(ctx, |ui| {
ui.heading("Temperature Range Analysis");
ui.separator();
ui.horizontal(|ui| {
ui.label("T0 (K):");
ui.text_edit_singleline(&mut self.t0);
ui.label("Tend (K):");
ui.text_edit_singleline(&mut self.tend);
if ui.button("Calculate Range").clicked() {
self.calculate_range_data();
}
});
ui.separator();
});
}
fn calculate_range_data(&mut self) {
let substance_name = if !self.selected_substance.is_empty() {
&self.selected_substance
} else {
&self.substance_input
};
if substance_name.is_empty() {
return;
}
let t0_result = self.t0.parse::<f64>();
let tend_result = self.tend.parse::<f64>();
match (t0_result, tend_result) {
(Ok(t0), Ok(tend)) if t0 < tend => {
if let Some(lib_data) = self.thermo_data.LibThermoData.get(&self.selected_library) {
if let Some(substance_data) = lib_data.get(substance_name) {
match self.calculate_temperature_range(substance_data, t0, tend, 100) {
Ok(results) => {
let temperatures: Vec<f64> =
results.iter().map(|(t, _, _, _)| *t).collect();
let t_result = DVector::from_vec(temperatures);
let mut y_data = Vec::new();
for (_, cp, dh, ds) in &results {
y_data.push(*cp); y_data.push(*dh); y_data.push(*ds); }
let y_result = DMatrix::from_row_slice(results.len(), 3, &y_data);
let (cp_unit, dh_unit, ds_unit) = match self.energy_unit {
EnergyUnit::J => ("Cp (J/mol·K)", "dH (J/mol)", "dS (J/mol·K)"),
EnergyUnit::Cal => {
("Cp (cal/mol·K)", "dH (cal/mol)", "dS (cal/mol·K)")
}
};
let values = vec![
cp_unit.to_string(),
dh_unit.to_string(),
ds_unit.to_string(),
];
self.plot_window = Some(PlotWindow::new(
"Temperature (K)".to_string(),
values,
t_result,
y_result,
));
}
Err(e) => {
println!("Range calculation error: {}", e);
}
}
}
}
}
_ => {
println!("Invalid temperature range");
}
}
}
pub fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new("Thermochemistry Analysis")
.open(open)
.default_size([1200.0, 800.0])
.show(ctx, |ui| {
ui.heading("Thermodynamic Properties and Calculations");
ui.separator();
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.set_width(200.0);
ui.set_min_height(600.0);
ui.heading("List of Substances");
ui.horizontal(|ui| {
ui.label("Search:");
ui.text_edit_singleline(&mut self.substance_input);
});
ui.separator();
egui::ScrollArea::vertical()
.max_height(1200.0)
.show(ui, |ui| {
for substance in &self.available_substances.clone() {
if self.substance_input.is_empty()
|| substance
.to_lowercase()
.contains(&self.substance_input.to_lowercase())
{
if ui.selectable_label(false, substance).clicked() {
self.search_substance_by_name(substance);
}
}
}
});
ui.separator();
egui::ComboBox::from_label("Library Source")
.selected_text(&self.selected_library)
.show_ui(ui, |ui| {
for library in &self.thermo_data.thermo_libs.clone() {
if ui
.selectable_value(
&mut self.selected_library,
library.clone(),
library,
)
.clicked()
{
self.update_substances_for_library();
}
}
});
});
ui.separator();
ui.vertical(|ui| {
ui.group(|ui| {
ui.label("Substance Information:");
egui::ScrollArea::vertical()
.id_salt("substance_info")
.max_height(300.0)
.show(ui, |ui| {
ui.text_edit_multiline(&mut self.search_results);
});
});
ui.add_space(10.0);
ui.group(|ui| {
ui.label("Calculation Parameters:");
ui.horizontal(|ui| {
ui.label("Temperature (K):");
ui.text_edit_singleline(&mut self.temperature);
ui.label("Pressure (Pa):");
ui.text_edit_singleline(&mut self.pressure);
if ui.button("🧮 Calculate Properties").clicked() {
self.calculate_properties();
}
});
ui.horizontal(|ui| {
ui.label("Energy Unit:");
ui.radio_value(&mut self.energy_unit, EnergyUnit::J, "J");
ui.radio_value(&mut self.energy_unit, EnergyUnit::Cal, "cal");
});
});
ui.separator();
ui.horizontal(|ui| {
if ui.button("Clear Results").clicked() {
self.search_results = "Results cleared".to_string();
}
if ui.button("View Plots").clicked() {
self.show_plots_window = true;
}
if ui.button("Export Data").clicked() {
self.search_results +=
"\n\nExport functionality not yet implemented";
}
if ui.button("Search in NIST").clicked() {
self.show_nist_window = true;
if self.nist_app.is_none() {
self.nist_app = Some(NISTApp::new());
}
}
});
});
});
if self.show_plots_window {
self.show_plots_window(ctx);
}
if let Some(plot_window) = &mut self.plot_window {
plot_window.show(ctx);
if !plot_window.visible {
self.plot_window = None;
}
}
if self.show_nist_window {
if let Some(nist_app) = &mut self.nist_app {
nist_app.show(ctx, &mut self.show_nist_window);
}
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_thermochemistry_app_new() {
let app = ThermochemistryApp::new();
assert!(app.substance_input.is_empty());
assert_eq!(app.temperature, "298.15");
assert_eq!(app.pressure, "101325.0");
assert!(!app.available_substances.is_empty());
}
#[test]
fn test_thermochemistry_app_default() {
let app = ThermochemistryApp::default();
assert!(app.substance_input.is_empty());
assert_eq!(app.temperature, "298.15");
assert_eq!(app.pressure, "101325.0");
assert_eq!(app.search_results, "No search performed yet");
}
#[test]
fn test_search_empty_substance() {
let mut app = ThermochemistryApp::new();
app.search_substance();
assert_eq!(app.search_results, "Please enter a substance name");
}
#[test]
fn test_calculate_properties_invalid_temperature() {
let mut app = ThermochemistryApp::new();
app.temperature = "invalid".to_string();
app.calculate_properties();
assert_eq!(app.search_results, "Please select a substance first");
}
}