KiThe 0.3.6

A numerical suite for chemical kinetics and thermodynamics, combustion, heat and mass transfer,chemical engeneering. Work in progress. Advices and contributions will be appreciated
#[cfg(test)]
mod tests {
    use super::super::SimpleReactorIVP::*;
    use crate::Kinetics::mechfinder_api::ReactionData;
    use approx::assert_abs_diff_eq;
    use std::collections::HashMap;

    fn build_condensed_task() -> SimpleReactorTask {
        let mut task = SimpleReactorTask::new();
        task.kindata
            .set_reaction_data_directly(
                vec![ReactionData::new_elementary(
                    "A=>B".to_string(),
                    vec![1.0e3, 0.0, 1.0e4],
                    None,
                )],
                None,
            )
            .expect("reaction setup should succeed");

        task.kindata.stecheodata.vec_of_molmasses = Some(vec![10.0, 20.0]);
        task.kindata.substances = vec!["A".to_string(), "B".to_string()];
        task.thermal_effects = vec![-2.0e5];
        task.ro = 1200.0;
        task.Cp = 1000.0;
        task.Lambda = 0.25;
        task.m = 0.015;
        task.scaling =
            crate::ReactorsBVP::reactor_BVP_utils::ScalingConfig::new(100.0, 0.05, 100.0);
        task.initial_conditions = HashMap::from([
            ("T".to_string(), 450.0),
            ("q".to_string(), 0.15),
            ("A".to_string(), 0.7),
            ("B".to_string(), 0.3),
        ]);
        task
    }

    #[test]
    fn test_condensed_setup_canonicalizes_spatial_coordinate() {
        let mut task = build_condensed_task();
        task.setup_condensed_ivp()
            .expect("condensed setup should succeed");

        assert_eq!(task.solver.arg_name, "x");
        assert_eq!(
            task.solver.x_range,
            (
                task.solver_backend_config.x0,
                task.solver_backend_config.x_bound
            )
        );

        let inputs = task
            .condensed_inputs()
            .expect("typed condensed inputs should build");
        assert_eq!(inputs.physical.ro, 1200.0);
        assert_eq!(inputs.initial_state.temperature, 450.0);
        assert_eq!(inputs.initial_state.heat_flux, 0.15);
        assert_eq!(inputs.initial_state.species.get("A"), Some(&0.7));
        assert_eq!(inputs.initial_state.species.get("B"), Some(&0.3));
    }

    #[test]
    fn test_canonical_state_names_follow_thermal_then_species_order() {
        let task = build_condensed_task();
        assert_eq!(
            task.canonical_state_names(),
            vec![
                "Teta".to_string(),
                "q".to_string(),
                "C0".to_string(),
                "C1".to_string()
            ]
        );
    }

    #[test]
    fn test_set_density_rejects_nonfinite_and_negative_values() {
        let mut task = SimpleReactorTask::new();
        assert!(task.set_density(1000.0).is_ok());
        assert!(task.set_density(0.0).is_err());
        assert!(task.set_density(-1.0).is_err());
        assert!(task.set_density(f64::NAN).is_err());
        assert_abs_diff_eq!(task.ro, 1000.0);
    }

    #[test]
    fn test_build_initial_state_vector_uses_canonical_order() {
        let task = build_condensed_task();
        let y0 = task
            .build_initial_state_vector()
            .expect("initial state vector should build");

        assert_eq!(y0.len(), 4);
        assert_abs_diff_eq!(y0[0], (450.0 - 100.0) / 100.0, epsilon = 1e-12);
        assert_abs_diff_eq!(y0[1], 0.15, epsilon = 1e-12);
        assert_abs_diff_eq!(y0[2], 0.7, epsilon = 1e-12);
        assert_abs_diff_eq!(y0[3], 0.3, epsilon = 1e-12);
    }

    #[test]
    fn test_setup_condensed_ivp_builds_species_only_snapshot() {
        let mut task = build_condensed_task();
        let result = task.setup_condensed_ivp();
        assert!(result.is_ok(), "{result:?}");

        assert_eq!(
            task.solver.unknowns,
            vec![
                "Teta".to_string(),
                "q".to_string(),
                "C0".to_string(),
                "C1".to_string()
            ]
        );
        assert_eq!(task.solver.eq_system.len(), 4);
        assert_eq!(task.map_of_equations.len(), 4);
        assert!(task.map_of_equations.contains_key("Teta"));
        assert!(task.map_of_equations.contains_key("q"));
        assert!(task.map_of_equations.contains_key("A"));
        assert!(task.map_of_equations.contains_key("B"));
        assert!(
            !task
                .solver
                .unknowns
                .iter()
                .any(|name| name.starts_with('J'))
        );
        assert!(
            !task
                .map_of_equations
                .keys()
                .any(|key| key.ends_with("_flux"))
        );
        assert!(
            !task
                .map_of_equations
                .values()
                .any(|(_, expr)| expr.to_string().contains("Pe_D"))
        );
    }

    #[test]
    fn test_setup_condensed_ivp_publishes_canonical_solver_shape() {
        let mut task = build_condensed_task();
        let result = task.setup_condensed_ivp();
        assert!(result.is_ok(), "{result:?}");

        assert_eq!(task.solver.arg_name, "x");
        assert_eq!(task.solver.unknowns.len(), 4);
        assert_eq!(task.solver.unknowns[0], "Teta");
        assert_eq!(task.solver.unknowns[1], "q");
    }

    #[test]
    fn test_condensed_inputs_bundle_captures_physical_and_initial_state_contract() {
        let task = build_condensed_task();
        let inputs = task
            .condensed_inputs()
            .expect("typed condensed inputs should build");

        assert_eq!(inputs.physical.ro, 1200.0);
        assert_eq!(inputs.physical.Cp, 1000.0);
        assert_eq!(inputs.physical.Lambda, 0.25);
        assert_eq!(inputs.physical.m, 0.015);
        assert_eq!(inputs.physical.L, 0.05);
        assert_eq!(inputs.initial_state.temperature, 450.0);
        assert_eq!(inputs.initial_state.heat_flux, 0.15);
        assert_eq!(inputs.initial_state.species.get("A"), Some(&0.7));
        assert_eq!(inputs.initial_state.species.get("B"), Some(&0.3));
        assert_eq!(inputs.thermal_effects, vec![-2.0e5]);
    }

    #[test]
    fn test_typed_initial_state_rejects_missing_species_entry() {
        let task = build_condensed_task();
        let mut inputs = task
            .condensed_inputs()
            .expect("typed condensed inputs should build");
        inputs.initial_state.species.remove("B");

        let result = inputs.validate(&task.kindata.substances);
        assert!(matches!(result, Err(IvpError::MissingData(_))));
    }

    #[test]
    fn test_validate_condensed_phase_contract_rejects_thermal_effect_mismatch() {
        let mut task = build_condensed_task();
        task.thermal_effects = vec![-2.0e5, 1.0];

        let result = task.validate_condensed_phase_contract();

        assert!(matches!(result, Err(IvpError::InvalidConfiguration(_))));
    }

    #[test]
    fn test_a_to_b_reaction_has_expected_species_source_signs() {
        let mut task = build_condensed_task();
        task.setup_condensed_ivp()
            .expect("condensed setup should succeed");

        let config = task
            .build_lsode2_problem_config()
            .expect("problem config should build");
        let names = config
            .values
            .iter()
            .map(|name| name.as_str())
            .collect::<Vec<_>>();
        let y0 = config.y0.as_slice();

        let rhs_a = config.eq_system[2].lower_to_linear(&names).eval(y0);
        let rhs_b = config.eq_system[3].lower_to_linear(&names).eval(y0);

        assert!(
            rhs_a > 0.0,
            "A should follow the current source convention, got rhs = {rhs_a}"
        );
        assert!(
            rhs_b < 0.0,
            "B should follow the current source convention, got rhs = {rhs_b}"
        );
    }
}