use crate::Kinetics::experimental_kinetics::testing_mod::{
AdvancedTGAConfig, ExperimentMode, KineticModel, NoiseConfig, NoiseKind, SpikeModel,
};
use crate::gui::experimental_kinetics_gui::gui_test::load_model_with_one_curve;
use crate::gui::experimental_kinetics_gui::help_view::{HelpDoc, HelpLanguage};
use crate::gui::experimental_kinetics_gui::model::PlotModel;
use crate::gui::experimental_kinetics_gui::settings::{CalibrationLine, Settings};
use std::collections::HashSet;
pub struct TestOptions {
show_settings_window: bool,
temp_k: String,
temp_b: String,
temp_n_points: String,
temp_log_level: String,
pub(crate) show_help_window: bool,
pub(crate) help_lang: HelpLanguage,
pub(crate) parsed_help: HelpDoc,
pub(crate) help_error: Option<String>,
pub(crate) help_status: Option<String>,
pub(crate) help_search_query: String,
pub(crate) help_selected_section: Option<usize>,
pub(crate) help_edit_mode: bool,
pub(crate) help_editor_buffer: String,
pub(crate) help_expanded_sections: std::collections::HashSet<usize>,
show_logs_window: bool,
show_synthetic_data: bool,
syn_n_points: String,
syn_dt: String,
syn_seed: String,
syn_mode: usize,
syn_iso_temperatures: String, syn_ni_t0: String,
syn_ni_heating_rates: String,
syn_kmodel: usize,
syn_m0: String,
syn_k0: String,
syn_e: String,
syn_r: String,
syn_m01: String,
syn_k01: String,
syn_e1: String,
syn_m02: String,
syn_k02: String,
syn_e2: String,
syn_r2: String,
syn_mass_noise_enabled: bool,
syn_mass_noise_kind: usize, syn_mass_sigma: String,
syn_mass_ampl: String,
syn_mass_slope: String,
syn_temp_noise_enabled: bool,
syn_temp_noise_kind: usize,
syn_temp_sigma: String,
syn_temp_ampl: String,
syn_temp_slope: String,
syn_spikes_enabled: bool,
syn_spike_prob: String,
syn_spike_ampl: String,
syn_status: Option<String>,
}
impl Default for TestOptions {
fn default() -> Self {
Self {
show_settings_window: false,
temp_k: String::new(),
temp_b: String::new(),
temp_n_points: String::new(),
temp_log_level: String::new(),
show_help_window: false,
help_lang: HelpLanguage::Eng,
parsed_help: HelpDoc::default(),
help_error: None,
help_status: None,
help_search_query: String::new(),
help_selected_section: None,
help_edit_mode: false,
help_editor_buffer: String::new(),
help_expanded_sections: HashSet::new(),
show_logs_window: false,
show_synthetic_data: false,
syn_n_points: "70000".to_string(),
syn_dt: "0.1".to_string(),
syn_seed: "42".to_string(),
syn_mode: 1,
syn_iso_temperatures: "400.0".to_string(),
syn_ni_t0: "400.0".to_string(),
syn_ni_heating_rates: "1.0, 2.0, 3.0".to_string(),
syn_kmodel: 0,
syn_m0: "100.0".to_string(),
syn_k0: "1e6".to_string(),
syn_e: "80000.0".to_string(),
syn_r: "8.314".to_string(),
syn_m01: "50.0".to_string(),
syn_k01: "1e6".to_string(),
syn_e1: "90000.0".to_string(),
syn_m02: "50.0".to_string(),
syn_k02: "1e3".to_string(),
syn_e2: "60000.0".to_string(),
syn_r2: "8.314".to_string(),
syn_mass_noise_enabled: false,
syn_mass_noise_kind: 0,
syn_mass_sigma: "5.0".to_string(),
syn_mass_ampl: "1.0".to_string(),
syn_mass_slope: "0.0".to_string(),
syn_temp_noise_enabled: false,
syn_temp_noise_kind: 0,
syn_temp_sigma: "0.1".to_string(),
syn_temp_ampl: "0.0".to_string(),
syn_temp_slope: "0.0".to_string(),
syn_spikes_enabled: false,
syn_spike_prob: "0.01".to_string(),
syn_spike_ampl: "1.0".to_string(),
syn_status: None,
}
}
}
impl TestOptions {
pub fn new() -> Self {
Self::default()
}
pub fn show(ui: &mut egui::Ui, model: &mut PlotModel, test_options: &mut TestOptions) {
if ui.button("Run Tests").clicked() {
println!("Stub: Run Tests");
}
if ui.button("Load testing data").clicked() {
println!("Stub: Load testing data");
model.push_testing_data();
}
if ui.button("Create synthetic data").clicked() {
test_options.show_synthetic_data = true;
}
if ui.button("Show and redact settings").clicked() {
test_options.show_settings_window = true;
test_options.load_current_settings(&model.settings);
}
if ui.button("Show Logs").clicked() {
test_options.show_logs_window = true;
}
ui.separator();
if ui.button("Help").clicked() {
println!("Opening help window");
test_options.show_help_window = true;
let _ = test_options.reload_help();
ui.ctx().request_repaint();
}
}
pub fn show_settings_window(&mut self, ctx: &egui::Context, model: &mut PlotModel) {
let mut is_open = self.show_settings_window;
egui::Window::new("TGA Settings")
.open(&mut is_open)
.resizable(true)
.default_width(400.0)
.show(ctx, |ui| {
ui.heading("Current Settings");
ui.separator();
ui.group(|ui| {
ui.label("Active Configuration:");
if let Some(cal) = model.settings.calibration_line() {
ui.label(format!("Calibration k: {}", cal.k()));
ui.label(format!("Calibration b: {}", cal.b()));
}
if let Some(n) = model.settings.n_points() {
ui.label(format!("Number of points: {}", n));
}
if let Some(log) = model.settings.log_level() {
ui.label(format!("Log level: {}", log));
}
});
ui.add_space(10.0);
ui.separator();
ui.heading("Edit Settings");
ui.group(|ui| {
ui.label("Calibration Line (mass = k * voltage + b):");
ui.horizontal(|ui| {
ui.label("k:");
ui.text_edit_singleline(&mut self.temp_k);
});
ui.horizontal(|ui| {
ui.label("b:");
ui.text_edit_singleline(&mut self.temp_b);
});
});
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label("Number of plot points:");
ui.text_edit_singleline(&mut self.temp_n_points);
});
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label("Log level:");
egui::ComboBox::from_id_salt("log_level_combo")
.selected_text(&self.temp_log_level)
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.temp_log_level, "off".to_string(), "off");
ui.selectable_value(
&mut self.temp_log_level,
"error".to_string(),
"error",
);
ui.selectable_value(
&mut self.temp_log_level,
"warn".to_string(),
"warn",
);
ui.selectable_value(
&mut self.temp_log_level,
"info".to_string(),
"info",
);
ui.selectable_value(
&mut self.temp_log_level,
"debug".to_string(),
"debug",
);
ui.selectable_value(
&mut self.temp_log_level,
"trace".to_string(),
"trace",
);
});
});
ui.add_space(10.0);
ui.separator();
ui.horizontal(|ui| {
if ui.button("Save Settings").clicked() {
if let Err(e) = self.apply_settings(model) {
eprintln!("Error applying settings: {:?}", e);
model.push_message(&format!("Error: {:?}", e)).ok();
} else {
model.push_message("Settings saved successfully").ok();
}
}
if ui.button("Reset to Defaults").clicked() {
model.settings = Settings::default();
self.load_current_settings(&model.settings);
model.push_message("Settings reset to defaults").ok();
}
if ui.button("Close").clicked() {
self.show_settings_window = false;
}
});
if !model.message.is_empty() {
ui.add_space(5.0);
ui.colored_label(egui::Color32::GREEN, &model.message);
}
});
self.show_settings_window = is_open;
}
pub fn show_synthetic_data_window(&mut self, ctx: &egui::Context, model: &mut PlotModel) {
if !self.show_synthetic_data {
return;
}
let mut open = self.show_synthetic_data;
egui::Window::new("Create Synthetic TGA Data")
.open(&mut open)
.resizable(true)
.vscroll(true)
.default_width(720.0)
.default_height(620.0)
.show(ctx, |ui| {
ui.heading("AdvancedTGAConfig");
ui.separator();
ui.group(|ui| {
ui.heading("General");
ui.horizontal(|ui| {
ui.label("n_points:");
ui.text_edit_singleline(&mut self.syn_n_points);
ui.label("dt (s):");
ui.text_edit_singleline(&mut self.syn_dt);
ui.label("seed:");
ui.text_edit_singleline(&mut self.syn_seed);
});
});
ui.add_space(6.0);
ui.group(|ui| {
ui.heading("Experiment Mode");
egui::ComboBox::from_id_salt("syn_mode")
.selected_text(match self.syn_mode {
0 => "Isothermal",
_ => "NonIsothermal",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.syn_mode, 0, "Isothermal");
ui.selectable_value(&mut self.syn_mode, 1, "NonIsothermal");
});
match self.syn_mode {
0 => {
ui.horizontal(|ui| {
ui.label("temperatures (K, csv):");
ui.text_edit_singleline(&mut self.syn_iso_temperatures);
});
}
_ => {
ui.horizontal(|ui| {
ui.label("t0 (K):");
ui.text_edit_singleline(&mut self.syn_ni_t0);
ui.label("heating_rates (K/min, csv):");
ui.text_edit_singleline(&mut self.syn_ni_heating_rates);
});
}
}
});
ui.add_space(6.0);
ui.group(|ui| {
ui.heading("Kinetic Model");
egui::ComboBox::from_id_salt("syn_kmodel")
.selected_text(match self.syn_kmodel {
0 => "ArrheniusSingle",
_ => "ArrheniusTwoComponent",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.syn_kmodel, 0, "ArrheniusSingle");
ui.selectable_value(&mut self.syn_kmodel, 1, "ArrheniusTwoComponent");
});
match self.syn_kmodel {
0 => {
egui::Grid::new("km_single").num_columns(8).show(ui, |ui| {
ui.label("m0");
ui.text_edit_singleline(&mut self.syn_m0);
ui.label("k0");
ui.text_edit_singleline(&mut self.syn_k0);
ui.label("E");
ui.text_edit_singleline(&mut self.syn_e);
ui.label("R");
ui.text_edit_singleline(&mut self.syn_r);
});
}
_ => {
egui::Grid::new("km_two").num_columns(14).show(ui, |ui| {
ui.label("m01");
ui.text_edit_singleline(&mut self.syn_m01);
ui.label("k01");
ui.text_edit_singleline(&mut self.syn_k01);
ui.label("E1");
ui.text_edit_singleline(&mut self.syn_e1);
ui.label("m02");
ui.text_edit_singleline(&mut self.syn_m02);
ui.label("k02");
ui.text_edit_singleline(&mut self.syn_k02);
ui.label("E2");
ui.text_edit_singleline(&mut self.syn_e2);
ui.label("R");
ui.text_edit_singleline(&mut self.syn_r2);
});
}
}
});
ui.add_space(6.0);
ui.group(|ui| {
ui.heading("Mass noise");
ui.checkbox(&mut self.syn_mass_noise_enabled, "enabled");
egui::ComboBox::from_id_salt("mass_noise_kind")
.selected_text(match self.syn_mass_noise_kind {
0 => "Gaussian",
1 => "Uniform",
_ => "Drift",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.syn_mass_noise_kind, 0, "Gaussian");
ui.selectable_value(&mut self.syn_mass_noise_kind, 1, "Uniform");
ui.selectable_value(&mut self.syn_mass_noise_kind, 2, "Drift");
});
match self.syn_mass_noise_kind {
0 => {
ui.horizontal(|ui| {
ui.label("sigma");
ui.text_edit_singleline(&mut self.syn_mass_sigma);
});
}
1 => {
ui.horizontal(|ui| {
ui.label("amplitude");
ui.text_edit_singleline(&mut self.syn_mass_ampl);
});
}
_ => {
ui.horizontal(|ui| {
ui.label("slope");
ui.text_edit_singleline(&mut self.syn_mass_slope);
});
}
}
ui.separator();
ui.heading("Temperature noise");
ui.checkbox(&mut self.syn_temp_noise_enabled, "enabled");
egui::ComboBox::from_id_salt("temp_noise_kind")
.selected_text(match self.syn_temp_noise_kind {
0 => "Gaussian",
1 => "Uniform",
_ => "Drift",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.syn_temp_noise_kind, 0, "Gaussian");
ui.selectable_value(&mut self.syn_temp_noise_kind, 1, "Uniform");
ui.selectable_value(&mut self.syn_temp_noise_kind, 2, "Drift");
});
match self.syn_temp_noise_kind {
0 => {
ui.horizontal(|ui| {
ui.label("sigma");
ui.text_edit_singleline(&mut self.syn_temp_sigma);
});
}
1 => {
ui.horizontal(|ui| {
ui.label("amplitude");
ui.text_edit_singleline(&mut self.syn_temp_ampl);
});
}
_ => {
ui.horizontal(|ui| {
ui.label("slope");
ui.text_edit_singleline(&mut self.syn_temp_slope);
});
}
}
ui.separator();
ui.heading("Spikes");
ui.checkbox(&mut self.syn_spikes_enabled, "enabled");
ui.horizontal(|ui| {
ui.label("probability");
ui.text_edit_singleline(&mut self.syn_spike_prob);
ui.label("amplitude");
ui.text_edit_singleline(&mut self.syn_spike_ampl);
});
});
if let Some(status) = &self.syn_status {
ui.colored_label(egui::Color32::YELLOW, status);
}
ui.separator();
ui.horizontal(|ui| {
if ui.button("Generate synthetic data").clicked() {
match self.try_build_config() {
Ok(cfg) => {
match model.generate_synthetic_data_from_config(&cfg) {
Ok(()) => {
self.syn_status =
Some("Synthetic data generated".to_string());
}
Err(e) => {
self.syn_status =
Some(format!("Error generating data: {:?}", e));
}
}
}
Err(err) => {
self.syn_status = Some(err);
}
}
}
if ui.button("Close").clicked() {
self.show_synthetic_data = false;
}
});
});
self.show_synthetic_data = open;
}
pub fn show_logs_window(&mut self, ctx: &egui::Context, model: &mut PlotModel) {
if !self.show_logs_window {
return;
}
let mut open = self.show_logs_window;
let mut feed = model
.history_feed_for_selected()
.unwrap_or_else(|err| format!("No history available: {:?}", err));
egui::Window::new("Operation History")
.open(&mut open)
.resizable(true)
.default_width(760.0)
.default_height(420.0)
.show(ctx, |ui| {
ui.label("Selected experiment history");
ui.separator();
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut feed)
.code_editor()
.desired_rows(20)
.interactive(false),
);
});
});
self.show_logs_window = open;
}
fn parse_csv_f64(s: &str) -> Result<Vec<f64>, String> {
let t = s.trim();
if t.is_empty() {
return Ok(Vec::new());
}
t.split([',', ';', ' '])
.filter(|p| !p.trim().is_empty())
.map(|p| {
p.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid float: {}", p))
})
.collect()
}
fn parse_bool_opt_noise(
kind: usize,
sigma: &str,
ampl: &str,
slope: &str,
) -> Result<NoiseConfig, String> {
let k = match kind {
0 => {
let s = sigma
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid sigma: {}", sigma))?;
NoiseKind::Gaussian { sigma: s }
}
1 => {
let a = ampl
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid amplitude: {}", ampl))?;
NoiseKind::Uniform { amplitude: a }
}
2 => {
let sl = slope
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid slope: {}", slope))?;
NoiseKind::Drift { slope: sl }
}
_ => return Err("Unknown noise kind".to_string()),
};
Ok(NoiseConfig { kind: k })
}
fn try_build_config(&self) -> Result<AdvancedTGAConfig, String> {
let n_points = self
.syn_n_points
.trim()
.parse::<usize>()
.map_err(|_| format!("Invalid n_points: {}", self.syn_n_points))?;
let dt = self
.syn_dt
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid dt: {}", self.syn_dt))?;
let seed = self
.syn_seed
.trim()
.parse::<u64>()
.map_err(|_| format!("Invalid seed: {}", self.syn_seed))?;
let experiment_mode = match self.syn_mode {
0 => {
let temps = Self::parse_csv_f64(&self.syn_iso_temperatures)?;
if temps.is_empty() {
return Err("Isothermal mode requires non-empty temperatures".to_string());
}
ExperimentMode::Isothermal {
temperatures: temps,
}
}
_ => {
let t0 = self
.syn_ni_t0
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid t0: {}", self.syn_ni_t0))?;
let betas = Self::parse_csv_f64(&self.syn_ni_heating_rates)?;
if betas.is_empty() {
return Err("NonIsothermal mode requires non-empty heating_rates".to_string());
}
ExperimentMode::NonIsothermal {
t0,
heating_rates: betas,
}
}
};
let kinetic_model = match self.syn_kmodel {
0 => {
let m0 = self
.syn_m0
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid m0: {}", self.syn_m0))?;
let k0 = self
.syn_k0
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid k0: {}", self.syn_k0))?;
let e = self
.syn_e
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid E: {}", self.syn_e))?;
let r = self
.syn_r
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid R: {}", self.syn_r))?;
KineticModel::ArrheniusSingle { m0, k0, e, r }
}
_ => {
let m01 = self
.syn_m01
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid m01: {}", self.syn_m01))?;
let k01 = self
.syn_k01
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid k01: {}", self.syn_k01))?;
let e1 = self
.syn_e1
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid e1: {}", self.syn_e1))?;
let m02 = self
.syn_m02
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid m02: {}", self.syn_m02))?;
let k02 = self
.syn_k02
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid k02: {}", self.syn_k02))?;
let e2 = self
.syn_e2
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid e2: {}", self.syn_e2))?;
let r = self
.syn_r2
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid R: {}", self.syn_r2))?;
KineticModel::ArrheniusTwoComponent {
m01,
k01,
e1,
m02,
k02,
e2,
r,
}
}
};
let mass_noise = if self.syn_mass_noise_enabled {
Some(Self::parse_bool_opt_noise(
self.syn_mass_noise_kind,
&self.syn_mass_sigma,
&self.syn_mass_ampl,
&self.syn_mass_slope,
)?)
} else {
None
};
let temp_noise = if self.syn_temp_noise_enabled {
Some(Self::parse_bool_opt_noise(
self.syn_temp_noise_kind,
&self.syn_temp_sigma,
&self.syn_temp_ampl,
&self.syn_temp_slope,
)?)
} else {
None
};
let spikes = if self.syn_spikes_enabled {
let p = self
.syn_spike_prob
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid spike probability: {}", self.syn_spike_prob))?;
let a = self
.syn_spike_ampl
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid spike amplitude: {}", self.syn_spike_ampl))?;
Some(SpikeModel {
probability: p,
amplitude: a,
})
} else {
None
};
Ok(AdvancedTGAConfig {
n_points,
dt,
experiment_mode,
kinetic_model,
mass_noise,
temp_noise,
spikes,
seed,
})
}
fn load_current_settings(&mut self, settings: &Settings) {
if let Some(cal) = settings.calibration_line() {
self.temp_k = cal.k().to_string();
self.temp_b = cal.b().to_string();
} else {
self.temp_k = "40.0".to_string();
self.temp_b = "-1.0".to_string();
}
self.temp_n_points = settings
.n_points()
.map(|n| n.to_string())
.unwrap_or_else(|| "1000".to_string());
self.temp_log_level = settings.log_level().unwrap_or("info").to_string();
}
fn apply_settings(&self, model: &mut PlotModel) -> Result<(), String> {
let k = self
.temp_k
.parse::<f64>()
.map_err(|_| format!("Invalid k value: '{}'", self.temp_k))?;
let b = self
.temp_b
.parse::<f64>()
.map_err(|_| format!("Invalid b value: '{}'", self.temp_b))?;
let n_points = self
.temp_n_points
.parse::<usize>()
.map_err(|_| format!("Invalid n_points value: '{}'", self.temp_n_points))?;
model
.settings
.set_calibration_line(Some(CalibrationLine::new(k, b)));
model.settings.set_n_points(Some(n_points));
model
.settings
.set_log_level(Some(self.temp_log_level.clone()));
model
.settings
.create_or_recreate_config()
.map_err(|e| format!("Failed to save settings: {:?}", e))?;
Ok(())
}
pub fn is_settings_window_open(&self) -> bool {
self.show_settings_window
}
}
impl PlotModel {
fn push_testing_data(&mut self) {
*self = load_model_with_one_curve();
}
}