use crate::parse::atom::{parse_atom_string, parse_zmatrix, AtomInfo, Unit};
use crate::parse::basis::{resolve_basis, BasisInput, BasisSpec};
use crate::parse::serde_build::parse_toml_input;
use crate::prelude::*;
use crate::util;
#[non_exhaustive]
#[derive(Debug, Clone, Builder, Default, Serialize, Deserialize)]
#[serde(default)]
#[builder(pattern = "owned", build_fn(error = "CIntError"), default)]
pub struct CIntMolInput {
#[builder(setter(into))]
pub atom: String,
#[builder(setter(into))]
pub basis: BasisSpec,
#[builder(setter(into), default = "BasisSpec::Uniform(BasisInput::None)")]
pub ecp: BasisSpec,
#[builder(setter(into), default = "Unit::Angstrom")]
pub unit: Unit,
#[builder(default = "false")]
pub cart: bool,
#[builder(default = "false")]
pub ghost_ecp: bool,
#[builder(default = "false")]
pub allow_empty_basis: bool,
}
#[derive(Debug, Clone)]
pub struct CIntMol {
pub atoms: Vec<AtomInfo>,
pub basis_elements: IndexMap<String, BseBasisElement>,
pub cint: CInt,
}
impl CIntMol {
pub fn from_json(json: &str) -> Self {
Self::from_json_f(json).cint_unwrap()
}
pub fn from_json_f(json: &str) -> Result<Self, CIntError> {
let input: CIntMolInput = serde_json::from_str(json).map_err(|e| cint_error!(ParseError, "Failed to parse JSON: {e}"))?;
if let BasisSpec::Uniform(BasisInput::String(s)) = &input.basis {
if s == "custom" {
return cint_raise!(ParseError, "JSON format does not support 'basis = \"custom\"' with separate custom table. Use inline dict format instead: `{{\"basis\": {{\"O\": \"STO-3G\"}}}}`");
}
}
if let BasisSpec::Uniform(BasisInput::String(s)) = &input.ecp {
if s == "custom" {
return cint_raise!(ParseError, "JSON format does not support 'ecp = \"custom\"' with separate custom table. Use inline dict format instead: `{{\"ecp\": {{\"Au\": \"LANL2DZ\"}}}}`");
}
}
input.create_mol_f()
}
#[doc = include_str!("from_toml_docs.md")]
pub fn from_toml(toml_str: &str) -> Self {
Self::from_toml_f(toml_str).cint_unwrap()
}
pub fn from_toml_f(toml_str: &str) -> Result<Self, CIntError> {
parse_toml_input(toml_str)
}
}
impl CIntMolInput {
pub fn create_mol_f(self) -> Result<CIntMol, CIntError> {
let atoms = self.parse_atoms()?;
if atoms.is_empty() {
return cint_raise!(ParseError, "No atoms provided");
}
let (basis_elements, atom_basis_map) = resolve_basis(&atoms, &self.basis, &self.ecp, self.ghost_ecp)?;
for (ia, symb) in atom_basis_map.iter().enumerate() {
let empty_basis = match basis_elements.get(symb) {
Some(basis) => basis.electron_shells.as_ref().is_none_or(|shells| shells.is_empty()),
None => true,
};
if empty_basis {
if !self.allow_empty_basis {
return cint_raise!(ParseError, "Atom {ia} symbol '{symb}' has no basis. Allow by setting `allow_empty_basis = true`.");
} else {
eprintln!("Warning: Atom {ia} symbol '{symb}' has no basis.");
}
}
}
const PTR_ENV_START: usize = crate::ffi::cint_ffi::PTR_ENV_START as usize;
let pre_env = vec![0.0; PTR_ENV_START]; let cint_type = if self.cart { CIntType::Cartesian } else { CIntType::Spheric };
let cint = make_env(&atoms, &basis_elements, &atom_basis_map, cint_type, pre_env)?;
let has_ecp = basis_elements.values().any(|b| b.ecp_electrons.is_some() && b.ecp_potentials.is_some());
let cint = if has_ecp { make_ecp_env(cint, &atoms, &basis_elements, &atom_basis_map)? } else { cint };
Ok(CIntMol { atoms, basis_elements, cint })
}
pub fn create_mol(self) -> CIntMol {
self.create_mol_f().cint_unwrap()
}
fn parse_atoms(&self) -> Result<Vec<AtomInfo>, CIntError> {
match parse_atom_string(&self.atom, self.unit) {
Ok(atoms) => Ok(atoms),
Err(e_atom) => match parse_zmatrix(&self.atom, self.unit) {
Ok(atoms) => Ok(atoms),
Err(e_zmat) => {
let msg =
format!("Failed to parse atoms:\n- Atom string error: {:?}\n- Z-matrix error: {:?}", e_atom.kind, e_zmat.kind);
let trace_msg = format!(
"Backtrace for atom parsing:\n- Atom string error:\n{:?}\n- Z-matrix error:\n{:?}",
e_atom.backtrace, e_zmat.backtrace
);
cint_raise!(ParseError, "{msg}\n======\n{trace_msg}")
},
},
}
}
}
fn make_atm_env(atom: &AtomInfo, ptr: usize) -> ([i32; 6], [f64; 4]) {
const CHARGE_OF: usize = crate::ffi::cint_ffi::CHARGE_OF as usize;
const PTR_COORD: usize = crate::ffi::cint_ffi::PTR_COORD as usize;
const POINT_NUC: i32 = crate::ffi::cint_ffi::POINT_NUC as i32;
const NUC_MOD_OF: usize = crate::ffi::cint_ffi::NUC_MOD_OF as usize;
const PTR_ZETA: usize = crate::ffi::cint_ffi::PTR_ZETA as usize;
let nuc_charge = atom.charge;
let mut atm = [0; 6];
let mut env = [0.0; 4];
atm[CHARGE_OF] = nuc_charge;
atm[PTR_COORD] = ptr as i32;
atm[NUC_MOD_OF] = POINT_NUC;
atm[PTR_ZETA] = (ptr + 3) as i32;
env[0..3].copy_from_slice(&atom.coords);
(atm, env)
}
fn make_bas_env(basis: &BseBasisElement, atom_id: usize, ptr: usize) -> (Vec<[i32; 8]>, Vec<f64>) {
let mut bas = Vec::new();
let mut env = Vec::new();
if let Some(shells) = basis.electron_shells.as_ref() {
shells.iter().for_each(|shell| {
shell.angular_momentum.iter().for_each(|&l| {
let exponents: Vec<f64> = shell.exponents.iter().map(|s| s.parse::<f64>().unwrap()).collect();
let coefficients: Vec<f64> = shell.coefficients.iter().flatten().map(|s| s.parse::<f64>().unwrap()).collect();
let nprim = exponents.len() as i32;
let nctr = coefficients.len() as i32 / nprim;
let normalized_coeffs = normalize_shell(l, &exponents, &coefficients);
let ptr_exp = (ptr + env.len()) as i32;
env.extend(exponents);
let ptr_coeff = (ptr + env.len()) as i32;
env.extend(normalized_coeffs);
bas.push([atom_id as i32, l, nprim, nctr, 0, ptr_exp, ptr_coeff, 0]);
});
});
}
(bas, env)
}
fn make_env(
atoms: &[AtomInfo],
basis_dict: &IndexMap<String, BseBasisElement>,
atom_basis_map: &[String],
cint_type: CIntType,
pre_env: Vec<f64>,
) -> Result<CInt, CIntError> {
const ATOM_OF: usize = crate::ffi::cint_ffi::ATOM_OF as usize;
assert_eq!(atoms.len(), atom_basis_map.len(), "Number of atoms and atom_basis_map entries must match");
let mut atm = vec![];
let mut bas = vec![];
let mut env = pre_env;
for atom in atoms.iter() {
let (atm_entry, env_entry) = make_atm_env(atom, env.len());
atm.push(atm_entry);
env.extend_from_slice(&env_entry);
}
let mut basdic = IndexMap::new();
for (symb, basis_add) in basis_dict.iter() {
if basis_add.electron_shells.is_none() {
continue;
}
let (bas_entries, env_entries) = make_bas_env(basis_add, 0, env.len());
basdic.insert(symb.clone(), bas_entries);
env.extend_from_slice(&env_entries);
}
for (ia, symb) in atom_basis_map.iter().enumerate() {
if let Some(bas_entries) = basdic.get(symb) {
for bas_entry in bas_entries.iter() {
let mut bas_entry = *bas_entry;
bas_entry[ATOM_OF] = ia as i32;
bas.push(bas_entry);
}
}
}
let mut cint = CInt::new();
cint.atm = atm;
cint.bas = bas;
cint.env = env;
cint.cint_type = cint_type;
Ok(cint)
}
fn make_ecp_env(
mut cint: CInt,
atoms: &[AtomInfo],
basis_dict: &IndexMap<String, BseBasisElement>,
atom_basis_map: &[String],
) -> Result<CInt, CIntError> {
const CHARGE_OF: usize = crate::ffi::cint_ffi::CHARGE_OF as usize;
const NUC_MOD_OF: usize = crate::ffi::cint_ffi::NUC_MOD_OF as usize;
const ATOM_OF: usize = crate::ffi::cint_ffi::ATOM_OF as usize;
const NUC_ECP: i32 = 4;
let mut ecpbas: Vec<[i32; 8]> = Vec::new();
let ptr_env = cint.env.len();
let mut ecpdic: IndexMap<String, (i32, Vec<[i32; 8]>)> = IndexMap::new();
let mut ptr = ptr_env;
for (symb, basis_add) in basis_dict.iter() {
if basis_add.ecp_electrons.is_none() || basis_add.ecp_potentials.is_none() {
continue;
}
let nelec = basis_add.ecp_electrons.unwrap();
let potentials = basis_add.ecp_potentials.as_ref().unwrap();
let mut ecp0: Vec<[i32; 8]> = Vec::new();
let max_ecp_am = *potentials.iter().flat_map(|p| p.angular_momentum.iter()).max().unwrap();
for potential in potentials.iter() {
for &l in potential.angular_momentum.iter() {
let exponents: Vec<f64> = potential.gaussian_exponents.iter().map(|s| s.parse::<f64>().unwrap()).collect();
let coefficients: Vec<Vec<f64>> =
potential.coefficients.iter().map(|v| v.iter().map(|s| s.parse::<f64>().unwrap()).collect()).collect();
let r_exponents: &[i32] = &potential.r_exponents;
assert!(matches!(coefficients.len(), 1 | 2), "Only support scalar ECP and SO-ECP with 1 or 2 sets of coefficients");
assert_eq!(r_exponents.len(), exponents.len());
assert_eq!(coefficients[0].len(), exponents.len());
if coefficients.len() == 2 {
assert_eq!(coefficients[1].len(), exponents.len());
}
let has_so_ecp = coefficients.len() > 1;
let l = if l == max_ecp_am { -1 } else { l };
let mut r_exp_map: BTreeMap<i32, Vec<usize>> = BTreeMap::new();
for (i, &r_exp) in r_exponents.iter().enumerate() {
r_exp_map.entry(r_exp).or_default().push(i);
}
for (r_order, idx_list) in r_exp_map.iter() {
let ecp_exp: Vec<f64> = idx_list.iter().map(|&i| exponents[i]).collect();
let ecp_coef: Vec<f64> = idx_list.iter().map(|&i| coefficients[0][i]).collect();
let nexp = ecp_exp.len() as i32;
let ptr_exp = ptr as i32;
cint.env.extend(&ecp_exp);
ptr += ecp_exp.len();
let ptr_coeff = ptr as i32;
cint.env.extend(&ecp_coef);
ptr += ecp_coef.len();
ecp0.push([0, l, nexp, *r_order, 0, ptr_exp, ptr_coeff, 0]);
if has_so_ecp {
let so_coef: Vec<f64> = idx_list.iter().map(|&i| coefficients[1][i]).collect();
let ptr_so_coeff = ptr as i32;
cint.env.extend(&so_coef);
ptr += so_coef.len();
ecp0.push([0, l, nexp, *r_order, 1, ptr_exp, ptr_so_coeff, 0]);
}
}
}
}
ecpdic.insert(symb.clone(), (nelec, ecp0));
}
if !ecpdic.is_empty() {
for (ia, symb) in atom_basis_map.iter().enumerate() {
if let Some((nelec, ecp_entries)) = ecpdic.get(symb) {
let original_charge = atoms[ia].charge;
cint.atm[ia][CHARGE_OF] = original_charge - *nelec;
cint.atm[ia][NUC_MOD_OF] = NUC_ECP;
for ecp_entry in ecp_entries.iter() {
let mut ecp_entry = *ecp_entry;
ecp_entry[ATOM_OF] = ia as i32;
ecpbas.push(ecp_entry);
}
}
}
}
cint.ecpbas = ecpbas;
Ok(cint)
}
fn normalize_shell(l: i32, exponents: &[f64], coefficients: &[f64]) -> Vec<f64> {
let nprim = exponents.len();
let nctr = coefficients.len() / nprim;
let prim_norms: Vec<f64> = exponents.iter().map(|&exp| gto_norm(l, exp)).collect();
let prim_normalized: Vec<f64> = (0..nprim * nctr)
.map(|idx| {
let i = idx / nprim; let j = idx % nprim; coefficients[i * nprim + j] * prim_norms[j]
})
.collect();
let mut normalized: Vec<f64> = Vec::with_capacity(coefficients.len());
for i in 0..nctr {
let mut contr_int = 0.0;
for j in 0..nprim {
for k in 0..nprim {
let cj = prim_normalized[i * nprim + j];
let ck = prim_normalized[i * nprim + k];
let ee_jk = util::gaussian_int((l * 2 + 2) as f64, exponents[j] + exponents[k]);
contr_int += cj * ck * ee_jk;
}
}
let contr_norm_factor = 1.0 / contr_int.sqrt();
for j in 0..nprim {
normalized.push(prim_normalized[i * nprim + j] * contr_norm_factor);
}
}
normalized
}
fn gto_norm(l: i32, exp: f64) -> f64 {
1.0 / util::gaussian_int((l * 2 + 2) as f64, 2.0 * exp).sqrt()
}