KiThe 0.3.4

A numerical suite for chemical kinetics and thermodynamics, combustion, heat and mass transfer,chemical engeneering. Work in progress. Advices and contributions will be appreciated
use crate::Kinetics::experimental_kinetics::one_experiment_dataset::{
    ColumnMeta, ColumnNature, ColumnOrigin, History, TGADataset, TGADomainError, TGASchema, Unit,
};
use crate::Kinetics::solid_state_kinetics_IVP::{KineticModelIVP, KineticModelNames};
use RustedSciThe::numerical::ODE_api2::SolverType;
use RustedSciThe::numerical::Radau::Radau_main::RadauOrder;
use egui_extras::{Column as TableColumn, TableBuilder};
use nalgebra::{DMatrix, DVector};
use polars::prelude::{Column as PolarsColumn, DataFrame, IntoLazy};
use std::collections::HashMap;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;

#[derive(Debug, Clone, PartialEq, EnumIter)]
pub enum SolverTypeShotcut {
    RadauOrder3,
    RadauOrder5,
    RadauOrder7,
    LSODE,
    NonstiffRK45,
    NonStiffDoPri,
    NonStiffAB4,
    BDF,
    BackwardEuler,
}

impl SolverTypeShotcut {
    pub fn get_solvertype(&self) -> SolverType {
        match self {
            Self::RadauOrder3 => SolverType::Radau(RadauOrder::Order3),
            Self::RadauOrder5 => SolverType::Radau(RadauOrder::Order5),
            Self::RadauOrder7 => SolverType::Radau(RadauOrder::Order7),
            Self::LSODE => SolverType::LSODE2,
            Self::NonstiffRK45 => SolverType::NonStiff("RK45".to_string()),
            Self::NonStiffDoPri => SolverType::NonStiff("DOPRI".to_string()),
            Self::NonStiffAB4 => SolverType::NonStiff("AB4".to_string()),
            Self::BDF => SolverType::BDF,
            Self::BackwardEuler => SolverType::BackwardEuler,
        }
    }
}

#[derive(Debug, Clone)]
pub struct IvpResult {
    pub time: DVector<f64>,
    pub values: DMatrix<f64>,
}

impl IvpResult {
    pub fn primary_series_name(&self) -> &'static str {
        "conversion"
    }
}

#[derive(Debug, Clone)]
pub struct IvpTaskState {
    pub selected_solver: SolverTypeShotcut,
    pub selected_model: Option<KineticModelNames>,
    pub required_params: Vec<String>,
    t_final_string: String,
    beta_string: String,
    t0_string: String,
    e_string: String,
    a_string: String,
    pub t_final: f64,
    pub beta: f64,
    pub t0: f64,
    pub e: f64,
    pub a: f64,
    param_strings: Vec<String>,
    params: Vec<f64>,
}

impl Default for IvpTaskState {
    fn default() -> Self {
        Self::new()
    }
}

impl IvpTaskState {
    pub fn new() -> Self {
        Self {
            selected_solver: SolverTypeShotcut::BDF,
            selected_model: None,
            required_params: Vec::new(),
            t_final_string: String::new(),
            beta_string: String::new(),
            t0_string: String::new(),
            e_string: String::new(),
            a_string: String::new(),
            t_final: 0.0,
            beta: 0.0,
            t0: 0.0,
            e: 0.0,
            a: 0.0,
            param_strings: Vec::new(),
            params: Vec::new(),
        }
    }

    pub fn select_model_clicked(&mut self, model: KineticModelNames) {
        self.required_params = model.required_params();
        self.param_strings = vec![String::new(); self.required_params.len()];
        self.params = vec![0.0; self.required_params.len()];
        self.selected_model = Some(model);
    }

    fn sync_param_buffers(&mut self) {
        let required = self.required_params.len();
        if self.param_strings.len() != required {
            self.param_strings.resize(required, String::new());
        }
        if self.params.len() != required {
            self.params.resize(required, 0.0);
        }
    }

    pub fn show_model_table(&mut self, ui: &mut egui::Ui) {
        TableBuilder::new(ui)
            .column(TableColumn::auto().resizable(true))
            .column(TableColumn::auto().resizable(true))
            .column(TableColumn::remainder())
            .header(20.0, |mut header| {
                header.col(|ui| {
                    ui.heading("Select");
                });
                header.col(|ui| {
                    ui.heading("Model name");
                });
                header.col(|ui| {
                    ui.heading("Formula");
                });
            })
            .body(|mut body| {
                for model in KineticModelNames::iter() {
                    body.row(20.0, |mut row| {
                        row.col(|ui| {
                            if ui.button("Select").clicked() {
                                self.select_model_clicked(model.clone());
                            }
                        });
                        row.col(|ui| {
                            ui.label(format!("{:?}", model));
                        });
                        row.col(|ui| {
                            ui.label(model.map_of_names_and_formulas());
                        });
                    });
                }
            });
    }

    pub fn show_solver_combo(&mut self, ui: &mut egui::Ui) {
        egui::ComboBox::from_label("Select Solver")
            .selected_text(format!("{:?}", self.selected_solver))
            .show_ui(ui, |ui| {
                for solver in SolverTypeShotcut::iter() {
                    ui.selectable_value(
                        &mut self.selected_solver,
                        solver.clone(),
                        format!("{:?}", solver),
                    );
                }
            });
    }

    pub fn show_parameter_inputs(&mut self, ui: &mut egui::Ui) {
        self.sync_param_buffers();
        ui.horizontal_wrapped(|ui| {
            for (idx, param) in self.required_params.iter().enumerate() {
                ui.label(param);
                if ui
                    .text_edit_singleline(&mut self.param_strings[idx])
                    .changed()
                {
                    if let Ok(parsed) = self.param_strings[idx].trim().parse::<f64>() {
                        self.params[idx] = parsed;
                    }
                }
                ui.label(format!("{}", self.params[idx]));
            }
        });
    }

    pub fn show_problem_inputs(&mut self, ui: &mut egui::Ui) {
        ui.horizontal(|ui| {
            ui.label("t_final:");
            ui.text_edit_singleline(&mut self.t_final_string);
            if let Ok(parsed) = self.t_final_string.trim().parse::<f64>() {
                self.t_final = parsed;
            }
            ui.label(self.t_final.to_string());
        });
        ui.horizontal(|ui| {
            ui.label("beta:");
            ui.text_edit_singleline(&mut self.beta_string);
            if let Ok(parsed) = self.beta_string.trim().parse::<f64>() {
                self.beta = parsed;
            }
            ui.label(self.beta.to_string());
        });
        ui.horizontal(|ui| {
            ui.label("T0:");
            ui.text_edit_singleline(&mut self.t0_string);
            if let Ok(parsed) = self.t0_string.trim().parse::<f64>() {
                self.t0 = parsed;
            }
            ui.label(self.t0.to_string());
        });
        ui.horizontal(|ui| {
            ui.label("E:");
            ui.text_edit_singleline(&mut self.e_string);
            if let Ok(parsed) = self.e_string.trim().parse::<f64>() {
                self.e = parsed;
            }
            ui.label(self.e.to_string());
        });
        ui.horizontal(|ui| {
            ui.label("A:");
            ui.text_edit_singleline(&mut self.a_string);
            if let Ok(parsed) = self.a_string.trim().parse::<f64>() {
                self.a = parsed;
            }
            ui.label(self.a.to_string());
        });
    }

    pub fn show_summary(&self, ui: &mut egui::Ui) {
        let text = format!(
            "Model: {:?}, A={}, E={}, beta={}, params={:?}, solver {:?}",
            self.selected_model, self.a, self.e, self.beta, self.params, self.selected_solver
        );
        ui.label(text);
    }

    pub fn run_kinetic_model(&mut self) -> Result<IvpResult, String> {
        let model = self
            .selected_model
            .clone()
            .ok_or_else(|| "No model selected".to_string())?;
        self.sync_param_buffers();

        let params = self
            .param_strings
            .iter()
            .enumerate()
            .map(|(idx, value)| {
                value.trim().parse::<f64>().map_err(|_| {
                    format!(
                        "Failed to parse parameter {} for {:?}: '{}'",
                        idx + 1,
                        model,
                        value
                    )
                })
            })
            .collect::<Result<Vec<_>, _>>()?;
        self.params = params.clone();

        let solver = self.selected_solver.get_solvertype();
        let mut kinetic_model = KineticModelIVP::new(solver);
        kinetic_model.set_problem(self.t_final, self.beta, self.t0, self.e, self.a)?;
        kinetic_model.set_model(model, params)?;
        kinetic_model.check_task()?;
        kinetic_model.solve()?;
        let (time, values) = kinetic_model.get_result()?;

        Ok(IvpResult { time, values })
    }
}

pub fn ivp_result_to_experiment(
    result: &IvpResult,
    experiment_id: String,
) -> Result<
    crate::Kinetics::experimental_kinetics::experiment_series_main::TGAExperiment,
    TGADomainError,
> {
    let time_values: Vec<f64> = result.time.iter().copied().collect();
    let mut columns: Vec<PolarsColumn> =
        vec![PolarsColumn::new("time".into(), time_values.as_slice())];
    let mut schema_columns = HashMap::new();
    schema_columns.insert(
        "time".to_string(),
        ColumnMeta::raw("time".to_string(), Unit::Second, ColumnNature::Time),
    );

    for col_idx in 0..result.values.ncols() {
        let values: Vec<f64> = result.values.column(col_idx).iter().copied().collect();
        let (name, nature) = if col_idx == 0 {
            (
                result.primary_series_name().to_string(),
                ColumnNature::Conversion,
            )
        } else {
            (format!("state_{}", col_idx + 1), ColumnNature::Unknown)
        };
        columns.push(PolarsColumn::new(name.clone().into(), values.as_slice()));
        schema_columns.insert(
            name.clone(),
            ColumnMeta::raw(name, Unit::Dimensionless, nature),
        );
    }

    let frame = DataFrame::new(time_values.len(), columns)?.lazy();
    let mut dataset = TGADataset {
        frame,
        schema: TGASchema {
            columns: schema_columns,
            time: Some("time".to_string()),
            temperature: None,
            mass: None,
            alpha: None,
            dm_dt: None,
            eta: None,
            deta_dt: None,
            dalpha_dt: None,
            dT_dt: None,
            E: None,
            R2: None,
        },
        oneframeplot: None,
        history_of_operations: History::new(),
        undo_stack: Vec::new(),
        undo_snapshot_latch: false,
    };
    dataset.initialize_column_provenance();

    Ok(
        crate::Kinetics::experimental_kinetics::experiment_series_main::TGAExperiment::new(dataset)
            .with_id(experiment_id),
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use nalgebra::{DMatrix, DVector};

    #[test]
    fn ivp_result_to_experiment_creates_conversion_column() {
        let result = IvpResult {
            time: DVector::from_vec(vec![0.0, 1.0, 2.0]),
            values: DMatrix::from_vec(3, 1, vec![0.0, 0.4, 1.0]),
        };

        let experiment = ivp_result_to_experiment(&result, "demo".to_string()).unwrap();
        assert_eq!(experiment.meta.id, "demo");
        assert!(experiment.dataset.get_column("time").is_ok());
        assert!(experiment.dataset.get_column("conversion").is_ok());
    }
}