use crate::interface::DFTD3Error;
use crate::parameters::{
convert_to_damping_param, get_default_param_table, get_merged_param_table, normalize_version,
DFTD3DampingParam,
};
use toml::Table;
const META_FIELDS: &[&str] = &["version", "method", "atm"];
#[cfg(feature = "api-v0_4")]
fn valid_fields_for_version(version: &str) -> Result<&[&str], DFTD3Error> {
match version {
"bj" => Ok(&["s6", "s8", "s9", "a1", "a2", "alp"]),
"zero" => Ok(&["s6", "s8", "s9", "rs6", "rs8", "alp"]),
"bjm" => Ok(&["s6", "s8", "s9", "a1", "a2", "alp"]),
"zerom" => Ok(&["s6", "s8", "s9", "rs6", "rs8", "alp", "bet"]),
#[cfg(feature = "api-v0_5")]
"op" => Ok(&["s6", "s8", "s9", "a1", "a2", "alp", "bet"]),
#[cfg(feature = "api-v1_3")]
"cso" => Ok(&["s6", "s9", "a1", "a2", "a3", "a4", "alp"]),
#[cfg(not(feature = "api-v0_5"))]
"op" => Err(DFTD3Error::ParametersError(format!(
"Variant '{version}' requires api-v0_5 feature"
))),
#[cfg(not(feature = "api-v1_3"))]
"cso" => Err(DFTD3Error::ParametersError(format!(
"Variant '{version}' requires api-v1_3 feature"
))),
_ => Err(DFTD3Error::ParametersError(format!("Unknown variant: {version}"))),
}
}
pub fn dftd3_parse_damping_param(input: &Table) -> DFTD3DampingParam {
dftd3_parse_damping_param_f(input).unwrap()
}
pub fn dftd3_parse_damping_param_f(input: &Table) -> Result<DFTD3DampingParam, DFTD3Error> {
let version_raw = input
.get("version")
.and_then(|v| v.as_str())
.ok_or_else(|| DFTD3Error::ParametersError("Missing required field 'version'".into()))?;
let version = normalize_version(version_raw);
let method = input.get("method").and_then(|v| v.as_str());
let atm = input.get("atm").and_then(|v| v.as_bool()).unwrap_or(true);
let s9_explicit = input.contains_key("s9");
#[cfg(feature = "api-v0_4")]
let valid_fields = valid_fields_for_version(&version)?;
#[cfg(not(feature = "api-v0_4"))]
{
match version.as_str() {
"bj" | "zero" | "bjm" | "zerom" => {
return Err(DFTD3Error::ParametersError(format!(
"Variant '{version}' requires api-v0_4 feature or higher"
)))
},
_ => return Err(DFTD3Error::ParametersError(format!("Unknown variant: {version}"))),
}
}
let user_param_keys: Vec<&str> =
input.keys().map(|k| k.as_str()).filter(|k| !META_FIELDS.contains(k)).collect();
for key in &user_param_keys {
if !valid_fields.contains(key) {
return Err(DFTD3Error::ParametersError(format!(
"Unknown parameter '{}' for variant '{}' (d3{})",
key, version, version
)));
}
}
let mut merged = if let Some(method) = method {
get_merged_param_table(method, &version)?
} else {
get_default_param_table(&version)?
};
for key in &user_param_keys {
if let Some(value) = input.get(*key) {
merged.insert((*key).to_string(), value.clone());
}
}
if !atm && !s9_explicit {
merged.insert("s9".to_string(), toml::Value::Float(0.0));
}
merged.remove("damping");
convert_to_damping_param(&merged, &version)
}
fn parse_toml_table(input: &str) -> Result<Table, DFTD3Error> {
let trimmed = input.trim();
if trimmed.starts_with('{') {
let wrapped = format!("x = {trimmed}");
let mut doc: Table = toml::from_str(&wrapped)
.map_err(|e| DFTD3Error::ParametersError(format!("TOML parsing error: {e}")))?;
if let Some(toml::Value::Table(table)) = doc.remove("x") {
Ok(table)
} else {
Err(DFTD3Error::ParametersError("Invalid TOML format".into()))
}
} else {
toml::from_str(trimmed)
.map_err(|e| DFTD3Error::ParametersError(format!("TOML parsing error: {e}")))
}
}
pub fn dftd3_parse_damping_param_from_toml(input: &str) -> DFTD3DampingParam {
dftd3_parse_damping_param_from_toml_f(input).unwrap()
}
pub fn dftd3_parse_damping_param_from_toml_f(input: &str) -> Result<DFTD3DampingParam, DFTD3Error> {
let table = parse_toml_table(input)?;
dftd3_parse_damping_param_f(&table)
}
#[cfg(feature = "json")]
pub fn dftd3_parse_damping_param_from_json(input: &str) -> DFTD3DampingParam {
dftd3_parse_damping_param_from_json_f(input).unwrap()
}
#[cfg(feature = "json")]
pub fn dftd3_parse_damping_param_from_json_f(input: &str) -> Result<DFTD3DampingParam, DFTD3Error> {
let value: serde_json::Value = serde_json::from_str(input)
.map_err(|e| DFTD3Error::ParametersError(format!("JSON parsing error: {e}")))?;
let table = json_value_to_toml_table(&value)?;
dftd3_parse_damping_param_f(&table)
}
#[cfg(feature = "json")]
fn json_value_to_toml_table(value: &serde_json::Value) -> Result<Table, DFTD3Error> {
match value {
serde_json::Value::Object(map) => {
let mut table = Table::new();
for (k, v) in map {
table.insert(k.clone(), json_value_to_toml(v)?);
}
Ok(table)
},
_ => Err(DFTD3Error::ParametersError("JSON root must be an object".into())),
}
}
#[cfg(feature = "json")]
fn json_value_to_toml(value: &serde_json::Value) -> Result<toml::Value, DFTD3Error> {
match value {
serde_json::Value::Null => {
Err(DFTD3Error::ParametersError("JSON null is not supported".into()))
},
serde_json::Value::Bool(b) => Ok(toml::Value::Boolean(*b)),
serde_json::Value::Number(n) => {
if let Some(f) = n.as_f64() {
Ok(toml::Value::Float(f))
} else if let Some(i) = n.as_i64() {
Ok(toml::Value::Integer(i))
} else {
Err(DFTD3Error::ParametersError("Unsupported JSON number".into()))
}
},
serde_json::Value::String(s) => Ok(toml::Value::String(s.clone())),
serde_json::Value::Array(arr) => {
let vec: Result<Vec<_>, _> = arr.iter().map(json_value_to_toml).collect();
Ok(toml::Value::Array(vec?))
},
serde_json::Value::Object(map) => {
let mut table = Table::new();
for (k, v) in map {
table.insert(k.clone(), json_value_to_toml(v)?);
}
Ok(toml::Value::Table(table))
},
}
}
#[test]
fn test_dftd3_parse_damping_param_from_toml_doc() {
use crate::prelude::*;
let input = r#"{version = "d3bj", method = "b3lyp"}"#;
let damping_param = dftd3_parse_damping_param_from_toml(input);
let dftd3_param = damping_param.new_param();
let atom_charges = vec![8, 1, 1];
#[rustfmt::skip]
let coordinates = vec![
0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 1.807355,
1.807355, 0.000000, -0.452500,
];
let model = DFTD3Model::new(&atom_charges, &coordinates, None, None);
let res = model.get_dispersion(&dftd3_param, false);
let eng = res.energy;
println!("Dispersion energy: {eng}");
let input = r#"{version = "d3bj", a1 = 0.3981, s8 = 1.9889, a2 = 4.4211, atm = false}"#;
let damping_param = dftd3_parse_damping_param_from_toml(input);
let dftd3_param = damping_param.new_param();
let res = model.get_dispersion(&dftd3_param, false);
let eng = res.energy;
println!("Dispersion energy with custom params: {eng}");
}