Skip to main content

KiThe/gui/experimental_kinetics_gui/
controller_methods.rs

1//! Controller code for Kinetic Methods menu and sub-windows.
2//!
3//! Each menu item opens a dedicated popup window that currently contains a placeholder
4//! for future implementation.
5
6use crate::Kinetics::experimental_kinetics::kinetic_methods::KineticMethod;
7use crate::Kinetics::experimental_kinetics::kinetic_methods::Kissinger::Kissinger;
8use crate::Kinetics::experimental_kinetics::kinetic_methods::combined::CombinedKineticAnalysis;
9use crate::Kinetics::experimental_kinetics::kinetic_methods::is_this_a_sublimation::{
10    IsThisASublimationResult, SublimationMethod,
11};
12use crate::Kinetics::experimental_kinetics::kinetic_methods::isoconversion::{
13    IsoconversionalKineticMethod, IsoconversionalMethod,
14};
15use crate::gui::experimental_kinetics_gui::model::{PlotModel, TGAGUIError};
16use eframe::egui;
17
18/// State for which Kinetic Methods tool windows are currently open.
19pub struct KineticMethodsWindowState {
20    pub isoconversional_open: bool,
21    pub isoconversional_method: IsoconversionalMethod,
22    pub isoconversional_status: String,
23    pub isoconversional_use_selected_experiments: bool,
24    pub isoconversional_selected_experiments: Vec<String>,
25    pub isoconversional_output_id: String,
26    pub kissinger_open: bool,
27    pub kissinger_status: String,
28    pub kissinger_ea_kj: Option<f64>,
29    pub kissinger_r2: Option<f64>,
30    pub kissinger_kinetic_expression: String,
31    pub combined_kinetics_open: bool,
32    pub combined_status: String,
33    pub combined_n_min: f64,
34    pub combined_n_max: f64,
35    pub combined_n_steps: usize,
36    pub combined_m_min: f64,
37    pub combined_m_max: f64,
38    pub combined_m_steps: usize,
39    pub combined_eta_min: f64,
40    pub combined_eta_max: f64,
41    pub combined_refinement_steps: usize,
42    pub combined_result_n: Option<f64>,
43    pub combined_result_m: Option<f64>,
44    pub combined_result_ea_kj: Option<f64>,
45    pub combined_result_r2: Option<f64>,
46    pub criado_master_curve_open: bool,
47    pub fit_model_open: bool,
48    pub sublimation_check_open: bool,
49    pub sublimation_status: String,
50    pub sublimation_mean_ea_kj: Option<f64>,
51    pub sublimation_mean_k: Option<f64>,
52    pub sublimation_verdict: Option<String>,
53    pub methods_golden_pipeline: bool,
54}
55
56impl Default for KineticMethodsWindowState {
57    fn default() -> Self {
58        Self {
59            isoconversional_open: false,
60            isoconversional_method: IsoconversionalMethod::default(),
61            isoconversional_status: String::new(),
62            isoconversional_use_selected_experiments: false,
63            isoconversional_selected_experiments: Vec::new(),
64            isoconversional_output_id: "isoconversional_result".to_string(),
65            kissinger_open: false,
66            kissinger_status: String::new(),
67            kissinger_ea_kj: None,
68            kissinger_r2: None,
69            kissinger_kinetic_expression: String::new(),
70            combined_kinetics_open: false,
71            combined_status: String::new(),
72            combined_n_min: CombinedKineticAnalysis::default().n_min,
73            combined_n_max: CombinedKineticAnalysis::default().n_max,
74            combined_n_steps: CombinedKineticAnalysis::default().n_steps,
75            combined_m_min: CombinedKineticAnalysis::default().m_min,
76            combined_m_max: CombinedKineticAnalysis::default().m_max,
77            combined_m_steps: CombinedKineticAnalysis::default().m_steps,
78            combined_eta_min: CombinedKineticAnalysis::default().eta_min,
79            combined_eta_max: CombinedKineticAnalysis::default().eta_max,
80            combined_refinement_steps: CombinedKineticAnalysis::default().refinement_steps,
81            combined_result_n: None,
82            combined_result_m: None,
83            combined_result_ea_kj: None,
84            combined_result_r2: None,
85            criado_master_curve_open: false,
86            fit_model_open: false,
87            sublimation_check_open: false,
88            sublimation_status: String::new(),
89            sublimation_mean_ea_kj: None,
90            sublimation_mean_k: None,
91            sublimation_verdict: None,
92            methods_golden_pipeline: false,
93        }
94    }
95}
96
97pub struct KineticMethods;
98
99impl KineticMethods {
100    /// Render the dropdown menu entries under the "Kinetic Methods" top menu.
101    pub fn show_menu(ui: &mut egui::Ui, state: &mut KineticMethodsWindowState) {
102        if ui.button("Isoconversional").clicked() {
103            state.isoconversional_open = true;
104        }
105        if ui.button("Kissinger").clicked() {
106            state.kissinger_open = true;
107        }
108        if ui.button("Combined Kinetics Analysis").clicked() {
109            state.combined_kinetics_open = true;
110        }
111        if ui.button("Criado Master Curve").clicked() {
112            state.criado_master_curve_open = true;
113        }
114        if ui.button("Fit Model").clicked() {
115            state.fit_model_open = true;
116        }
117        if ui.button("Is this a sublimation?").clicked() {
118            state.sublimation_check_open = true;
119        }
120        if ui.button("Golden Pipeline").clicked() {
121            state.methods_golden_pipeline = true;
122        }
123    }
124    //===========================================================================================================
125    /// ISOCONVERSIONAL ANALYSIS
126    ///===========================================================================================================
127    /// Show the isoconversional analysis window.
128    pub fn show_isoconversional_window(
129        ui: &mut egui::Ui,
130        model: &mut PlotModel,
131        state: &mut KineticMethodsWindowState,
132    ) -> Result<(), TGAGUIError> {
133        if !state.isoconversional_open {
134            return Ok(());
135        }
136
137        egui::Window::new("Isoconversional")
138            .open(&mut state.isoconversional_open)
139            .default_size([540.0, 360.0])
140            .show(ui.ctx(), |ui| {
141                ui.vertical(|ui| {
142                    ui.horizontal(|ui| {
143                        ui.label("Method:");
144                        egui::ComboBox::from_id_salt("isoconv_method")
145                            .selected_text(state.isoconversional_method.display_name())
146                            .show_ui(ui, |ui| {
147                                for method in IsoconversionalMethod::all() {
148                                    ui.selectable_value(
149                                        &mut state.isoconversional_method,
150                                        *method,
151                                        method.display_name(),
152                                    );
153                                }
154                            });
155                    });
156
157                    ui.add_space(6.0);
158
159                    // Build a stable list of experiment ids for selection.
160                    let exp_ids = model.list_of_experiments();
161                    state
162                        .isoconversional_selected_experiments
163                        .retain(|id| exp_ids.iter().any(|exp| exp == id));
164
165                    ui.horizontal(|ui| {
166                        ui.label("Experiments:");
167                        ui.checkbox(
168                            &mut state.isoconversional_use_selected_experiments,
169                            "Use selected only",
170                        );
171                    });
172
173                    // When enabled, the user chooses a subset of experiments.
174                    if state.isoconversional_use_selected_experiments {
175                        if exp_ids.is_empty() {
176                            ui.label("No experiments available.");
177                        } else {
178                            ui.vertical(|ui| {
179                                for id in &exp_ids {
180                                    let mut selected = state
181                                        .isoconversional_selected_experiments
182                                        .iter()
183                                        .any(|v| v == id);
184                                    if ui.checkbox(&mut selected, id).changed() {
185                                        if selected {
186                                            if !state
187                                                .isoconversional_selected_experiments
188                                                .contains(id)
189                                            {
190                                                state
191                                                    .isoconversional_selected_experiments
192                                                    .push(id.clone());
193                                            }
194                                        } else {
195                                            state
196                                                .isoconversional_selected_experiments
197                                                .retain(|v| v != id);
198                                        }
199                                    }
200                                }
201                            });
202                        }
203                    }
204
205                    ui.add_space(6.0);
206
207                    ui.horizontal(|ui| {
208                        ui.label("Output ID:");
209                        ui.text_edit_singleline(&mut state.isoconversional_output_id);
210                    });
211
212                    ui.add_space(6.0);
213
214                    ui.horizontal(|ui| {
215                        ui.label("Status:");
216                        ui.label(&state.isoconversional_status);
217                    });
218
219                    ui.add_space(8.0);
220
221                    // Run the chosen method and store the result as a new experiment.
222                    if ui.button("Calculate").clicked() {
223                        let status = (|| -> Result<String, TGAGUIError> {
224                            if exp_ids.is_empty() {
225                                return Ok("No experiments to analyze.".to_string());
226                            }
227
228                            let selected_ids: Vec<&str> = state
229                                .isoconversional_selected_experiments
230                                .iter()
231                                .map(|s| s.as_str())
232                                .collect();
233
234                            let what_exp_to_take = if state.isoconversional_use_selected_experiments
235                            {
236                                if selected_ids.is_empty() {
237                                    return Ok("Select at least one experiment.".to_string());
238                                }
239                                Some(selected_ids.as_slice())
240                            } else {
241                                None
242                            };
243
244                            let output_id = if state.isoconversional_output_id.trim().is_empty() {
245                                "isoconversional_result".to_string()
246                            } else {
247                                state.isoconversional_output_id.trim().to_string()
248                            };
249
250                            let view = model.create_kinetic_data_view_for_method(
251                                what_exp_to_take,
252                                &state.isoconversional_method,
253                            )?;
254
255                            let method = IsoconversionalKineticMethod {
256                                method: state.isoconversional_method.clone(),
257                            };
258
259                            method.check_input(&view)?;
260                            let result = method.compute(&view)?;
261
262                            model.push_isoconversional_result(&result, &output_id)?;
263
264                            Ok(format!("Done. Result saved as '{}'.", output_id))
265                        })();
266
267                        state.isoconversional_status = match status {
268                            Ok(msg) => msg,
269                            Err(err) => format!("Error: {:?}", err),
270                        };
271                    }
272                });
273            });
274
275        Ok(())
276    }
277    //===========================================================================================================
278    /// KISSINGER ANALYSIS
279    //===========================================================================================================
280    /// Show the Kissinger analysis window.
281    pub fn show_kissinger_window(
282        ui: &mut egui::Ui,
283        model: &mut PlotModel,
284        state: &mut KineticMethodsWindowState,
285    ) -> Result<(), TGAGUIError> {
286        if !state.kissinger_open {
287            return Ok(());
288        }
289
290        egui::Window::new("Kissinger")
291            .open(&mut state.kissinger_open)
292            .default_size([520.0, 300.0])
293            .show(ui.ctx(), |ui| {
294                ui.vertical(|ui| {
295                    let ea_label = state
296                        .kissinger_ea_kj
297                        .map(|v| format!("{:.2}", v))
298                        .unwrap_or_else(|| "-".to_string());
299                    let r2_label = state
300                        .kissinger_r2
301                        .map(|v| format!("{:.4}", v))
302                        .unwrap_or_else(|| "-".to_string());
303
304                    ui.horizontal(|ui| {
305                        ui.label("Ea (kJ/mol):");
306                        ui.label(ea_label);
307                    });
308
309                    ui.horizontal(|ui| {
310                        ui.label("R2:");
311                        ui.label(r2_label);
312                    });
313
314                    ui.add_space(6.0);
315
316                    ui.horizontal(|ui| {
317                        ui.label("System messages:");
318                        ui.label(&state.kissinger_status);
319                    });
320
321                    ui.add_space(8.0);
322
323                    // Placeholder for future work on pre-exponential factor estimation.
324                    ui.label("Kinetic expression:");
325                    ui.text_edit_singleline(&mut state.kissinger_kinetic_expression);
326                    if ui.button("Calculate kinetic expression").clicked() {
327                        todo!("Implement pre-exponential factor estimation for Kissinger method.");
328                    }
329
330                    if ui.button("Calculate pre-exponential factor").clicked() {
331                        todo!("Implement pre-exponential factor estimation for Kissinger method.");
332                    }
333
334                    ui.add_space(8.0);
335
336                    // Run the Kissinger calculation without creating new series data or curves.
337                    if ui.button("Calculate").clicked() {
338                        let status = (|| -> Result<String, TGAGUIError> {
339                            // Kissinger needs the same columns as non-isothermal isoconversional methods.
340                            let view = model.create_kinetic_data_view_for_method(
341                                None,
342                                &IsoconversionalMethod::OFW,
343                            )?;
344
345                            let result = Kissinger.compute(&view)?;
346
347                            let ea_kj = result.ea / 1000.0;
348                            state.kissinger_ea_kj = Some(ea_kj);
349                            state.kissinger_r2 = Some(result.regression.r2);
350
351                            Ok("Done.".to_string())
352                        })();
353
354                        state.kissinger_status = match status {
355                            Ok(msg) => msg,
356                            Err(err) => format!("Error: {:?}", err),
357                        };
358                    }
359                });
360            });
361
362        Ok(())
363    }
364    //===========================================================================================================
365    /// SUBLIMATION CHECK
366    //===========================================================================================================
367    /// Show the "Is this a sublimation?" window.
368    pub fn show_sublimation_window(
369        ui: &mut egui::Ui,
370        model: &mut PlotModel,
371        state: &mut KineticMethodsWindowState,
372    ) -> Result<(), TGAGUIError> {
373        if !state.sublimation_check_open {
374            return Ok(());
375        }
376
377        egui::Window::new("Is this a sublimation?")
378            .open(&mut state.sublimation_check_open)
379            .default_size([560.0, 320.0])
380            .show(ui.ctx(), |ui| {
381                ui.vertical(|ui| {
382                    let ea_label = state
383                        .sublimation_mean_ea_kj
384                        .map(|v| format!("{:.2}", v))
385                        .unwrap_or_else(|| "-".to_string());
386                    let k_label = state
387                        .sublimation_mean_k
388                        .map(|v| format!("{:.4e}", v))
389                        .unwrap_or_else(|| "-".to_string());
390                    let verdict_label = state
391                        .sublimation_verdict
392                        .clone()
393                        .unwrap_or_else(|| "-".to_string());
394
395                    ui.horizontal(|ui| {
396                        ui.label("Mean Ea (kJ/mol):");
397                        ui.label(ea_label);
398                    });
399
400                    ui.horizontal(|ui| {
401                        ui.label("Mean k:");
402                        ui.label(k_label);
403                    });
404
405                    ui.horizontal(|ui| {
406                        ui.label("Verdict:");
407                        ui.label(verdict_label);
408                    });
409
410                    ui.add_space(6.0);
411
412                    ui.horizontal(|ui| {
413                        ui.label("System messages:");
414                        ui.label(&state.sublimation_status);
415                    });
416
417                    ui.add_space(8.0);
418
419                    // Run the solver, store mean values, and push fitted curves into the series.
420                    if ui.button("Calculate").clicked() {
421                        let status = (|| -> Result<String, TGAGUIError> {
422                            let method = SublimationMethod::default();
423                            let cols = method.required_columns_by_nature();
424                            let view = model.create_kinetic_data_view(None, cols)?;
425
426                            let results = method.compute(&view)?;
427                            let (mean_ea, mean_k, verdict) =
428                                SublimationMethod::mean_results(&results)?;
429
430                            state.sublimation_mean_ea_kj = Some(mean_ea / 1000.0);
431                            state.sublimation_mean_k = Some(mean_k);
432
433                            let verdict_text = match verdict {
434                                IsThisASublimationResult::Yes(r2) => {
435                                    format!("Yes (R2={:.4})", r2)
436                                }
437                                IsThisASublimationResult::MayBe(r2) => {
438                                    format!("Maybe (R2={:.4})", r2)
439                                }
440                                IsThisASublimationResult::No(r2) => {
441                                    format!("No (R2={:.4})", r2)
442                                }
443                            };
444                            state.sublimation_verdict = Some(verdict_text);
445
446                            model.push_sublimation_fitted_rates(&results)?;
447
448                            Ok("Done.".to_string())
449                        })();
450
451                        state.sublimation_status = match status {
452                            Ok(msg) => msg,
453                            Err(err) => format!("Error: {:?}", err),
454                        };
455                    }
456                });
457            });
458
459        Ok(())
460    }
461    //===========================================================================================================
462    /// COMBINED KINETICS ANALYSIS
463    //===========================================================================================================
464    /// Show the Combined Kinetics Analysis window.
465    pub fn show_combined_kinetics_window(
466        ui: &mut egui::Ui,
467        model: &mut PlotModel,
468        state: &mut KineticMethodsWindowState,
469    ) -> Result<(), TGAGUIError> {
470        if !state.combined_kinetics_open {
471            return Ok(());
472        }
473
474        egui::Window::new("Combined Kinetics Analysis")
475            .open(&mut state.combined_kinetics_open)
476            .default_size([560.0, 420.0])
477            .show(ui.ctx(), |ui| {
478                ui.vertical(|ui| {
479                    // Manual parameter inputs for the analysis grid.
480                    ui.horizontal(|ui| {
481                        ui.label("n_min:");
482                        ui.add(egui::DragValue::new(&mut state.combined_n_min).speed(0.1));
483                        ui.label("n_max:");
484                        ui.add(egui::DragValue::new(&mut state.combined_n_max).speed(0.1));
485                        ui.label("n_steps:");
486                        ui.add(egui::DragValue::new(&mut state.combined_n_steps).speed(1.0));
487                    });
488
489                    ui.horizontal(|ui| {
490                        ui.label("m_min:");
491                        ui.add(egui::DragValue::new(&mut state.combined_m_min).speed(0.1));
492                        ui.label("m_max:");
493                        ui.add(egui::DragValue::new(&mut state.combined_m_max).speed(0.1));
494                        ui.label("m_steps:");
495                        ui.add(egui::DragValue::new(&mut state.combined_m_steps).speed(1.0));
496                    });
497
498                    ui.horizontal(|ui| {
499                        ui.label("alpha_min:");
500                        ui.add(egui::DragValue::new(&mut state.combined_eta_min).speed(0.01));
501                        ui.label("alpha_max:");
502                        ui.add(egui::DragValue::new(&mut state.combined_eta_max).speed(0.01));
503                        ui.label("refinement_steps:");
504                        ui.add(
505                            egui::DragValue::new(&mut state.combined_refinement_steps).speed(1.0),
506                        );
507                    });
508
509                    ui.add_space(6.0);
510
511                    let n_label = state
512                        .combined_result_n
513                        .map(|v| format!("{:.4}", v))
514                        .unwrap_or_else(|| "-".to_string());
515                    let m_label = state
516                        .combined_result_m
517                        .map(|v| format!("{:.4}", v))
518                        .unwrap_or_else(|| "-".to_string());
519                    let ea_label = state
520                        .combined_result_ea_kj
521                        .map(|v| format!("{:.2}", v))
522                        .unwrap_or_else(|| "-".to_string());
523                    let r2_label = state
524                        .combined_result_r2
525                        .map(|v| format!("{:.4}", v))
526                        .unwrap_or_else(|| "-".to_string());
527
528                    ui.horizontal(|ui| {
529                        ui.label("n:");
530                        ui.label(n_label);
531                        ui.label("m:");
532                        ui.label(m_label);
533                    });
534
535                    ui.horizontal(|ui| {
536                        ui.label("Ea (kJ/mol):");
537                        ui.label(ea_label);
538                        ui.label("R2:");
539                        ui.label(r2_label);
540                    });
541
542                    ui.add_space(6.0);
543
544                    ui.horizontal(|ui| {
545                        ui.label("System messages:");
546                        ui.label(&state.combined_status);
547                    });
548
549                    ui.add_space(8.0);
550
551                    // Run the analysis and update result labels.
552                    if ui.button("Calculate").clicked() {
553                        let status = (|| -> Result<String, TGAGUIError> {
554                            let analysis = CombinedKineticAnalysis {
555                                n_min: state.combined_n_min,
556                                n_max: state.combined_n_max,
557                                n_steps: state.combined_n_steps,
558                                m_min: state.combined_m_min,
559                                m_max: state.combined_m_max,
560                                m_steps: state.combined_m_steps,
561                                eta_min: state.combined_eta_min,
562                                eta_max: state.combined_eta_max,
563                                refinement_steps: state.combined_refinement_steps,
564                            };
565
566                            let cols = analysis.required_columns_by_nature();
567                            let view = model.create_kinetic_data_view(None, cols)?;
568                            let result = analysis.compute(&view)?;
569
570                            state.combined_result_n = Some(result.n);
571                            state.combined_result_m = Some(result.m);
572                            state.combined_result_ea_kj = Some(result.ea / 1000.0);
573                            state.combined_result_r2 = Some(result.regression.r2);
574
575                            Ok("Done.".to_string())
576                        })();
577
578                        state.combined_status = match status {
579                            Ok(msg) => msg,
580                            Err(err) => format!("Error: {:?}", err),
581                        };
582                    }
583                });
584            });
585
586        Ok(())
587    }
588    //==============================================================================================
589    // END OF KINETIC METHODS WINDOWS
590    //==============================================================================================
591    // COMBINED KINETICS ANALYSIS
592    //===================================================================================================
593    /// Render any open method windows.
594    pub fn show_windows(
595        ui: &mut egui::Ui,
596        model: &mut PlotModel,
597        state: &mut KineticMethodsWindowState,
598    ) -> Result<(), TGAGUIError> {
599        // Separate helper functions keep each window small and focused.
600        Self::show_isoconversional_window(ui, model, state)?;
601        Self::show_kissinger_window(ui, model, state)?;
602        Self::show_combined_kinetics_window(ui, model, state)?;
603        Self::show_sublimation_window(ui, model, state)?;
604
605        if state.criado_master_curve_open {
606            egui::Window::new("Criado Master Curve")
607                .open(&mut state.criado_master_curve_open)
608                .default_size([520.0, 340.0])
609                .show(ui.ctx(), |ui| {
610                    ui.label("TODO: Implement Criado master curve analysis.");
611                });
612        }
613
614        if state.fit_model_open {
615            egui::Window::new("Fit Model")
616                .open(&mut state.fit_model_open)
617                .default_size([480.0, 320.0])
618                .show(ui.ctx(), |ui| {
619                    ui.label("TODO: Implement model fitting.");
620                });
621        }
622
623        Ok(())
624    }
625}