use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::Read;
use std::sync::{Arc, Mutex, OnceLock};
use crate::Kinetics::error::{KineticsError, KineticsResult};
use crate::library_manager::with_library_manager;
use serde_json::Value;
type LibraryReactbase = HashMap<String, HashMap<String, Value>>;
type LibraryReactionDb = HashMap<String, HashMap<String, HashMap<String, HashSet<String>>>>;
static REACTBASE_CACHE: OnceLock<Mutex<Option<Arc<LibraryReactbase>>>> = OnceLock::new();
static REACTION_DB_CACHE: OnceLock<Mutex<Option<Arc<LibraryReactionDb>>>> = OnceLock::new();
fn cache_lock_error(cache_name: &str) -> KineticsError {
KineticsError::InvalidReactionData(format!("failed to lock {} cache", cache_name))
}
pub(crate) fn cached_reactbase() -> KineticsResult<Arc<LibraryReactbase>> {
let cache = REACTBASE_CACHE.get_or_init(|| Mutex::new(None));
{
let guard = cache.lock().map_err(|_| cache_lock_error("Reactbase"))?;
if let Some(existing) = guard.as_ref() {
return Ok(existing.clone());
}
}
let mut file = with_library_manager(|manager| File::open(manager.reactbase_path()))?;
let mut file_contents = String::new();
file.read_to_string(&mut file_contents)?;
let parsed: LibraryReactbase = serde_json::from_str(&file_contents)?;
let parsed = Arc::new(parsed);
let mut guard = cache.lock().map_err(|_| cache_lock_error("Reactbase"))?;
if let Some(existing) = guard.as_ref() {
return Ok(existing.clone());
}
*guard = Some(parsed.clone());
Ok(parsed)
}
pub(crate) fn cached_reaction_db() -> KineticsResult<Arc<LibraryReactionDb>> {
let cache = REACTION_DB_CACHE.get_or_init(|| Mutex::new(None));
{
let guard = cache
.lock()
.map_err(|_| cache_lock_error("reaction database"))?;
if let Some(existing) = guard.as_ref() {
return Ok(existing.clone());
}
}
let mut file = with_library_manager(|manager| File::open(manager.dict_reaction_path()))?;
let mut file_contents = String::new();
file.read_to_string(&mut file_contents)?;
let parsed: LibraryReactionDb = serde_json::from_str(&file_contents)?;
let parsed = Arc::new(parsed);
let mut guard = cache
.lock()
.map_err(|_| cache_lock_error("reaction database"))?;
if let Some(existing) = guard.as_ref() {
return Ok(existing.clone());
}
*guard = Some(parsed.clone());
Ok(parsed)
}
#[derive(Debug, Clone)]
pub struct KineticData {
pub HashMapOfReactantsAndProducts: HashMap<String, HashMap<String, HashSet<String>>>, pub lib_name: String,
pub LibKineticData: HashMap<String, Value>, pub AllLibraries: Vec<String>, pub AllEquations: Vec<String>, pub EquationReactionMap: HashMap<String, String>, pub UserEquationReactionMap: HashMap<String, String>,
pub FoundReactionsByProducts: Vec<String>,
pub FoundReactionsByReagents: Vec<String>,
pub FoundReactionDatasByIDs: Vec<Value>,
}
impl KineticData {
pub fn new() -> Self {
Self {
HashMapOfReactantsAndProducts: HashMap::new(),
lib_name: String::new(),
LibKineticData: HashMap::new(),
AllLibraries: Vec::new(),
AllEquations: Vec::new(),
EquationReactionMap: HashMap::new(),
UserEquationReactionMap: HashMap::new(),
FoundReactionsByProducts: Vec::new(),
FoundReactionsByReagents: Vec::new(),
FoundReactionDatasByIDs: Vec::new(),
}
}
pub fn open_json_files(&mut self, big_mech: &str) -> KineticsResult<()> {
self.lib_name = big_mech.to_owned();
let reactlibrary = cached_reactbase()?;
self.AllLibraries = reactlibrary.keys().map(|k| k.to_string()).collect();
let library_of_kinetic_parameters = reactlibrary
.get(big_mech)
.ok_or_else(|| KineticsError::MissingLibrary(big_mech.to_string()))?;
self.LibKineticData = library_of_kinetic_parameters.to_owned();
let reaction_db = cached_reaction_db()?;
let library_of_reagents_and_products: &HashMap<String, HashMap<String, HashSet<String>>> =
reaction_db
.get(big_mech)
.ok_or_else(|| KineticsError::MissingLibrary(big_mech.to_string()))?;
self.HashMapOfReactantsAndProducts = library_of_reagents_and_products.to_owned();
Ok(())
}
pub fn print_all_reactions(&mut self) -> KineticsResult<()> {
let all_reactions: Vec<String> =
self.LibKineticData.keys().map(|k| k.to_string()).collect();
let all_equations: Vec<String> = self
.LibKineticData
.keys()
.map(|k| {
self.LibKineticData
.get(k)
.and_then(|reaction| reaction.get("eq"))
.and_then(|eq| eq.as_str())
.map(|eq| eq.to_string())
.ok_or_else(|| KineticsError::MissingReaction(k.clone()))
})
.collect::<KineticsResult<Vec<String>>>()?;
self.AllEquations = all_equations.clone();
let EquationReactionIDMap: HashMap<String, String> = all_equations
.iter()
.zip(all_reactions.iter())
.map(|(eq, r)| (eq.to_string(), r.to_string()))
.collect();
self.EquationReactionMap = EquationReactionIDMap;
Ok(())
} pub fn search_reaction_by_equation(
&mut self,
equation: &str,
) -> KineticsResult<(String, Value)> {
let reaction_id = self
.EquationReactionMap
.get(equation)
.ok_or_else(|| KineticsError::MissingReaction(equation.to_string()))?;
let reaction = self
.LibKineticData
.get(reaction_id)
.ok_or_else(|| KineticsError::MissingReaction(reaction_id.clone()))?;
return Ok((reaction_id.clone(), reaction.clone()));
}
fn search_reaction_by_substances(
&mut self,
substances: Vec<String>,
SubstanceType: &str,
) -> KineticsResult<Vec<String>> {
let substances: HashSet<String> = substances.iter().cloned().collect();
let mut found_reactions: Vec<String> = Vec::new();
for (r_id, reaction_parts) in &self.HashMapOfReactantsAndProducts {
let Some(base_substances) = reaction_parts.get(SubstanceType) else {
return Err(KineticsError::MissingReaction(r_id.to_string()));
};
if base_substances
.iter()
.filter(|subs| !matches!(subs.as_str(), "M" | "M)" | "(M" | "(M)"))
.all(|subs| substances.contains(subs))
{
found_reactions.push(r_id.to_string());
}
}
Ok(found_reactions)
}
pub fn search_reaction_by_reagents_and_products(
&mut self,
reagents: Vec<String>,
) -> KineticsResult<()> {
let found_reactions_by_reagents =
self.search_reaction_by_substances(reagents.clone(), "reagents")?;
self.FoundReactionsByReagents = found_reactions_by_reagents;
let found_reactions_by_products =
self.search_reaction_by_substances(reagents, "products")?;
self.FoundReactionsByProducts = found_reactions_by_products;
Ok(())
}
pub fn search_reactdata_by_reaction_id(&mut self, reaction_id: &str) -> KineticsResult<Value> {
let reaction = self
.LibKineticData
.get(reaction_id)
.ok_or_else(|| KineticsError::MissingReaction(reaction_id.to_string()))?;
return Ok(reaction.clone());
}
pub fn search_reactdata_for_vector_of_IDs(
&mut self,
reaction_ids: Vec<String>,
) -> KineticsResult<Vec<Value>> {
let mut vec_of_reactions: Vec<Value> = Vec::new();
for r_id in reaction_ids {
let reaction = self
.LibKineticData
.get(&r_id)
.ok_or_else(|| KineticsError::MissingReaction(r_id.clone()))?;
vec_of_reactions.push(reaction.clone());
}
self.FoundReactionDatasByIDs = vec_of_reactions.clone();
Ok(vec_of_reactions)
}
pub fn get_reactions_by_field(&self, field: &str, field_value: &str) -> Vec<Value> {
self.LibKineticData
.values()
.filter(|reaction| {
reaction
.get(field)
.and_then(|v| v.as_str())
.map_or(false, |v| v == field_value)
})
.cloned()
.collect()
}
#[allow(dead_code)]
pub fn form_user_chosen_data(
&self,
what_to_get: SearchVariants,
mech_name: Option<String>,
) -> KineticsResult<HashMap<String, HashMap<String, Value>>> {
let big_mech = if let Some(mech_name) = { mech_name } {
mech_name
} else {
self.lib_name.clone()
};
let reaction_ids = self.get_search_results(what_to_get);
if reaction_ids.len() == 0 {
return Ok(HashMap::new());
}
let mut hash_map_id_data = HashMap::with_capacity(reaction_ids.len());
for r_id in reaction_ids {
let reaction = self
.LibKineticData
.get(&r_id)
.ok_or_else(|| KineticsError::MissingReaction(r_id.clone()))?;
hash_map_id_data.insert(r_id, reaction.clone());
}
let mut hash_map = HashMap::with_capacity(1);
hash_map.insert(big_mech, hash_map_id_data);
Ok(hash_map)
}
#[allow(dead_code)]
pub fn get_search_results(&self, what_to_get: SearchVariants) -> Vec<String> {
match what_to_get {
SearchVariants::FoundReactionsByProducts => self.FoundReactionsByProducts.clone(),
SearchVariants::FoundReactionsByReagents => self.FoundReactionsByReagents.clone(),
}
}
}
pub enum SearchVariants {
FoundReactionsByProducts,
FoundReactionsByReagents,
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn test_kinetic_data_new() {
let mut kin_instance = KineticData::new();
let lib = "NUIG";
let LibVec: HashSet<String> = vec![
"Beckstead".to_string(),
"NUIG".to_string(),
"Cantera".to_string(),
"Beckstead_c".to_string(),
"Aramco".to_string(),
]
.into_iter()
.collect();
kin_instance.open_json_files(lib);
kin_instance.print_all_reactions();
assert_eq!(kin_instance.HashMapOfReactantsAndProducts.is_empty(), false);
assert_eq!(kin_instance.LibKineticData.is_empty(), false);
let all_lib: HashSet<String> = kin_instance.AllLibraries.clone().into_iter().collect();
assert_eq!(all_lib, LibVec);
assert_eq!(kin_instance.AllEquations.is_empty(), false);
let equation = kin_instance
.LibKineticData
.get("1")
.unwrap()
.get("eq")
.unwrap()
.as_str()
.unwrap()
.to_string();
let (reaction_id, reaction) = kin_instance.search_reaction_by_equation(&equation).unwrap();
assert_eq!(reaction_id, "1".to_string());
assert_eq!(reaction.get("eq").unwrap().as_str().unwrap(), equation);
let substances: Vec<String> = vec!["CO".to_string(), "O".to_string()];
kin_instance
.search_reaction_by_reagents_and_products(substances)
.unwrap();
assert_eq!(kin_instance.FoundReactionsByProducts.is_empty(), false);
assert_eq!(kin_instance.FoundReactionsByReagents.is_empty(), false);
kin_instance
.search_reactdata_for_vector_of_IDs(vec!["1".to_string(), "2".to_string()])
.unwrap();
assert_eq!(kin_instance.FoundReactionDatasByIDs.is_empty(), false);
}
#[test]
fn test_open_json_files_rejects_unknown_library() {
let mut kin_instance = KineticData::new();
let err = kin_instance.open_json_files("NotARealLibrary").unwrap_err();
assert!(matches!(
err,
KineticsError::MissingLibrary(library) if library == "NotARealLibrary"
));
}
#[test]
fn test_search_reactdata_by_reaction_id_rejects_missing_reaction() {
let mut kin_instance = KineticData::new();
kin_instance.open_json_files("NUIG").unwrap();
let err = kin_instance
.search_reactdata_by_reaction_id("definitely_missing_id")
.unwrap_err();
assert!(matches!(
err,
KineticsError::MissingReaction(reaction_id)
if reaction_id == "definitely_missing_id"
));
}
#[test]
fn test_search_reaction_by_equation_rejects_missing_equation() {
let mut kin_instance = KineticData::new();
kin_instance.open_json_files("NUIG").unwrap();
kin_instance.print_all_reactions().unwrap();
let err = kin_instance
.search_reaction_by_equation("A + B => C")
.unwrap_err();
assert!(matches!(
err,
KineticsError::MissingReaction(equation) if equation == "A + B => C"
));
}
#[test]
fn test_search_reactdata_for_vector_of_ids_rejects_missing_reaction() {
let mut kin_instance = KineticData::new();
kin_instance.open_json_files("NUIG").unwrap();
let err = kin_instance
.search_reactdata_for_vector_of_IDs(vec!["definitely_missing_id".to_string()])
.unwrap_err();
assert!(matches!(
err,
KineticsError::MissingReaction(reaction_id)
if reaction_id == "definitely_missing_id"
));
}
#[test]
fn test_search_value() {
let mut kin_instance = KineticData::new();
let lib = "NUIG";
kin_instance.open_json_files(lib);
kin_instance.print_all_reactions();
let data_of_reacion = kin_instance.LibKineticData.get("1").unwrap();
println!(" data_of_reacion {:?}", data_of_reacion);
}
#[test]
fn test_search_data_by_field() {
let mut kin_instance = KineticData::new();
kin_instance.open_json_files("NUIG");
let reactions_by_field = kin_instance.get_reactions_by_field("type", "falloff");
println!(" reactions_by_field {:?}", reactions_by_field);
assert_eq!(reactions_by_field.len(), 109);
}
#[test]
fn test_form_user_chosen_data() {
let mut kin_instance = KineticData::new();
kin_instance.open_json_files("NUIG");
kin_instance.FoundReactionsByProducts = vec!["1".to_string(), "2".to_string()];
kin_instance.FoundReactionsByReagents = vec!["3".to_string(), "4".to_string()];
let result = kin_instance
.form_user_chosen_data(SearchVariants::FoundReactionsByProducts, None)
.unwrap();
assert_eq!(result.len(), 1);
assert!(result.contains_key("NUIG"));
let nuig_data = result.get("NUIG").unwrap();
assert_eq!(nuig_data.len(), 2);
assert!(nuig_data.contains_key("1"));
assert!(nuig_data.contains_key("2"));
let result = kin_instance
.form_user_chosen_data(
SearchVariants::FoundReactionsByReagents,
Some("CustomMech".to_string()),
)
.unwrap();
assert_eq!(result.len(), 1);
assert!(result.contains_key("CustomMech"));
let custom_mech_data = result.get("CustomMech").unwrap();
assert_eq!(custom_mech_data.len(), 2);
assert!(custom_mech_data.contains_key("3"));
assert!(custom_mech_data.contains_key("4"));
kin_instance.FoundReactionsByProducts = vec![];
let result = kin_instance
.form_user_chosen_data(SearchVariants::FoundReactionsByProducts, None)
.unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_form_user_chosen_data_integration() {
let mut kin_instance = KineticData::new();
kin_instance.open_json_files("NUIG");
kin_instance
.search_reaction_by_reagents_and_products(vec!["O".to_string(), "H".to_string()])
.unwrap();
let result = kin_instance
.form_user_chosen_data(SearchVariants::FoundReactionsByProducts, None)
.unwrap();
assert!(!result.is_empty());
assert!(result.contains_key("NUIG"));
let nuig_data = result.get("NUIG").unwrap();
assert!(!nuig_data.is_empty());
for (reaction_id, reaction_data) in nuig_data {
assert_eq!(
reaction_data,
kin_instance.LibKineticData.get(reaction_id).unwrap()
);
}
}
#[test]
fn test_cached_reactbase_reuses_same_allocation() {
let first = cached_reactbase().unwrap();
let second = cached_reactbase().unwrap();
assert!(Arc::ptr_eq(&first, &second));
}
#[test]
fn test_cached_reaction_db_reuses_same_allocation() {
let first = cached_reaction_db().unwrap();
let second = cached_reaction_db().unwrap();
assert!(Arc::ptr_eq(&first, &second));
}
}