use super::basis::{build_sto3g_basis, ANG_TO_BOHR};
use super::cis::{compute_cis_with_dipole, CisResult};
use super::d3::compute_d3_energy;
use super::fock::nuclear_repulsion;
use super::gcp::compute_gcp;
use super::integrals::compute_eris;
use super::nuclear::compute_nuclear_matrix;
use super::overlap_kin::{compute_kinetic_matrix, compute_overlap_matrix};
use super::scf::{solve_scf, ScfConfig};
use super::srb::compute_srb;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HfConfig {
pub max_scf_iter: usize,
pub diis_size: usize,
pub n_cis_states: usize,
pub corrections: bool,
}
impl Default for HfConfig {
fn default() -> Self {
HfConfig {
max_scf_iter: 300,
diis_size: 6,
n_cis_states: 5,
corrections: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hf3cResult {
pub energy: f64,
pub hf_energy: f64,
pub nuclear_repulsion: f64,
pub d3_energy: f64,
pub gcp_energy: f64,
pub srb_energy: f64,
pub orbital_energies: Vec<f64>,
pub scf_iterations: usize,
pub converged: bool,
pub cis: Option<CisResult>,
pub n_basis: usize,
pub n_electrons: usize,
pub homo_energy: f64,
pub lumo_energy: Option<f64>,
pub gap: f64,
pub mulliken_charges: Vec<f64>,
}
#[cfg(feature = "experimental-gpu")]
fn hf_basis_to_gpu_basis(basis: &super::basis::BasisSet) -> crate::scf::basis::BasisSet {
use crate::scf::basis::{
BasisFunction as GpuBasisFunction, BasisSet as GpuBasisSet,
ContractedShell as GpuContractedShell, GaussianPrimitive,
};
let mut functions = Vec::new();
let mut shells = Vec::new();
let mut function_to_atom = Vec::new();
for shell in &basis.shells {
let primitives: Vec<GaussianPrimitive> = shell
.exponents
.iter()
.zip(shell.coefficients.iter())
.map(|(&alpha, &coefficient)| GaussianPrimitive { alpha, coefficient })
.collect();
let l = match shell.shell_type {
super::basis::ShellType::S => 0,
super::basis::ShellType::P => 1,
};
shells.push(GpuContractedShell {
atom_index: shell.center_idx,
center: shell.center,
l,
primitives: primitives.clone(),
});
match shell.shell_type {
super::basis::ShellType::S => {
functions.push(GpuBasisFunction {
atom_index: shell.center_idx,
center: shell.center,
angular: [0, 0, 0],
l_total: 0,
primitives: primitives.clone(),
});
function_to_atom.push(shell.center_idx);
}
super::basis::ShellType::P => {
for angular in [[1, 0, 0], [0, 1, 0], [0, 0, 1]] {
functions.push(GpuBasisFunction {
atom_index: shell.center_idx,
center: shell.center,
angular,
l_total: 1,
primitives: primitives.clone(),
});
function_to_atom.push(shell.center_idx);
}
}
}
}
let n_basis = functions.len();
GpuBasisSet {
functions,
shells,
n_basis,
function_to_atom,
}
}
pub fn solve_hf3c(
elements: &[u8],
positions: &[[f64; 3]],
config: &HfConfig,
) -> Result<Hf3cResult, String> {
if elements.len() != positions.len() {
return Err("elements/positions length mismatch".to_string());
}
if elements.is_empty() {
return Err("empty molecule".to_string());
}
let pos_bohr: Vec<[f64; 3]> = positions
.iter()
.map(|p| [p[0] * ANG_TO_BOHR, p[1] * ANG_TO_BOHR, p[2] * ANG_TO_BOHR])
.collect();
let basis = build_sto3g_basis(elements, positions);
let n_basis = basis.n_basis();
let n_electrons: usize = elements.iter().map(|&z| z as usize).sum();
let s_mat = compute_overlap_matrix(&basis);
let t_mat = compute_kinetic_matrix(&basis);
let v_mat = compute_nuclear_matrix(&basis, elements, &pos_bohr);
let h_core = &t_mat + &v_mat;
let eris = compute_eris(&basis);
#[cfg(feature = "experimental-gpu")]
let gpu_eris_full = if n_basis >= 4 {
let n4 = (n_basis as u64)
.saturating_mul(n_basis as u64)
.saturating_mul(n_basis as u64)
.saturating_mul(n_basis as u64);
let mem_bytes = n4.saturating_mul(8);
let max_mem: u64 = 512 * 1024 * 1024;
if mem_bytes > max_mem {
None } else if let Ok(ctx) = crate::gpu::context::GpuContext::try_create() {
let gpu_basis = hf_basis_to_gpu_basis(&basis);
crate::gpu::two_electron_gpu::compute_eris_gpu(&ctx, &gpu_basis)
.ok()
.map(|gpu_eris| {
let cap = n_basis * n_basis * n_basis * n_basis;
let mut full = Vec::with_capacity(cap);
for mu in 0..n_basis {
for nu in 0..n_basis {
for lam in 0..n_basis {
for sig in 0..n_basis {
full.push(gpu_eris.get(mu, nu, lam, sig));
}
}
}
}
full
})
} else {
None
}
} else {
None
};
#[cfg(not(feature = "experimental-gpu"))]
let gpu_eris_full: Option<Vec<f64>> = None;
let scf_config = ScfConfig {
max_iter: config.max_scf_iter,
diis_size: config.diis_size,
..ScfConfig::default()
};
let scf_result = solve_scf(
&h_core,
&s_mat,
&eris,
gpu_eris_full.as_deref(),
n_electrons,
&scf_config,
);
let e_nuc = nuclear_repulsion(elements, &pos_bohr);
let (d3_e, gcp_e, srb_e) = if config.corrections {
(
compute_d3_energy(elements, &pos_bohr).energy,
compute_gcp(elements, &pos_bohr),
compute_srb(elements, &pos_bohr),
)
} else {
(0.0, 0.0, 0.0)
};
let total = scf_result.energy + e_nuc + d3_e + gcp_e + srb_e;
let cis = if config.n_cis_states > 0 && scf_result.converged {
let n_occ = n_electrons / 2;
let ao_map = super::basis::ao_to_atom_map(&basis);
Some(compute_cis_with_dipole(
&scf_result.orbital_energies,
&scf_result.coefficients,
&eris,
n_basis,
n_occ,
config.n_cis_states,
Some(&pos_bohr),
Some(&ao_map),
))
} else {
None
};
let n_occ = n_electrons / 2;
let homo_energy = if n_occ > 0 && n_occ <= scf_result.orbital_energies.len() {
scf_result.orbital_energies[n_occ - 1]
} else {
0.0
};
let lumo_energy = if n_occ < scf_result.orbital_energies.len() {
Some(scf_result.orbital_energies[n_occ])
} else {
None
};
let gap = lumo_energy.map_or(0.0, |l| l - homo_energy);
let mulliken_charges = if scf_result.converged {
let ps = &scf_result.density * &s_mat;
let ao_to_atom = super::basis::ao_to_atom_map(&basis);
let mut charges = vec![0.0_f64; elements.len()];
for mu in 0..n_basis {
charges[ao_to_atom[mu]] += ps[(mu, mu)];
}
charges
.iter()
.enumerate()
.map(|(i, &pop)| elements[i] as f64 - pop)
.collect()
} else {
vec![0.0; elements.len()]
};
Ok(Hf3cResult {
energy: total,
hf_energy: scf_result.energy + e_nuc,
nuclear_repulsion: e_nuc,
d3_energy: d3_e,
gcp_energy: gcp_e,
srb_energy: srb_e,
orbital_energies: scf_result.orbital_energies,
scf_iterations: scf_result.iterations,
converged: scf_result.converged,
cis,
n_basis,
n_electrons,
homo_energy,
lumo_energy,
gap,
mulliken_charges,
})
}
#[cfg(feature = "parallel")]
pub fn solve_hf3c_batch(
molecules: &[(&[u8], &[[f64; 3]])],
config: &HfConfig,
) -> Vec<Result<Hf3cResult, String>> {
use rayon::prelude::*;
molecules
.par_iter()
.map(|(els, pos)| solve_hf3c(els, pos, config))
.collect()
}
#[cfg(not(feature = "parallel"))]
pub fn solve_hf3c_batch(
molecules: &[(&[u8], &[[f64; 3]])],
config: &HfConfig,
) -> Vec<Result<Hf3cResult, String>> {
molecules
.iter()
.map(|(els, pos)| solve_hf3c(els, pos, config))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_hf3c_iteration_budget() {
assert_eq!(HfConfig::default().max_scf_iter, 300);
}
#[test]
fn test_h2_hf3c() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.74]];
let config = HfConfig {
n_cis_states: 0,
..Default::default()
};
let result = solve_hf3c(&elements, &positions, &config).unwrap();
assert!(result.energy.is_finite(), "Energy should be finite");
assert!(result.energy < 0.0, "H2 total energy should be negative");
}
#[test]
fn test_water_hf3c() {
let elements = [8u8, 1, 1];
let positions = [
[0.0, 0.0, 0.117],
[0.0, 0.757, -0.469],
[0.0, -0.757, -0.469],
];
let result = solve_hf3c(&elements, &positions, &HfConfig::default()).unwrap();
assert!(result.energy.is_finite());
assert!(result.orbital_energies.len() == 7); }
}