1use 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
18pub 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 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 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 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 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 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 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 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 if ui.button("Calculate").clicked() {
338 let status = (|| -> Result<String, TGAGUIError> {
339 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 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 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 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 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 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 pub fn show_windows(
595 ui: &mut egui::Ui,
596 model: &mut PlotModel,
597 state: &mut KineticMethodsWindowState,
598 ) -> Result<(), TGAGUIError> {
599 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}