use nalgebra::DMatrix;
use super::basis::BasisSet;
use super::constants::{
DIIS_SUBSPACE_SIZE, HARTREE_TO_EV, SCF_DENSITY_THRESHOLD, SCF_ENERGY_THRESHOLD, SCF_MAX_ITER,
};
use super::core_matrices::{nuclear_repulsion_energy, CoreMatrices};
use super::diis::DiisAccelerator;
use super::orthogonalization::{back_transform, lowdin_orthogonalization, transform_to_orthogonal};
use super::two_electron::TwoElectronIntegrals;
use super::types::MolecularSystem;
#[derive(Debug, Clone)]
pub struct UhfResult {
pub alpha_orbital_energies: Vec<f64>,
pub beta_orbital_energies: Vec<f64>,
pub alpha_coefficients: DMatrix<f64>,
pub beta_coefficients: DMatrix<f64>,
pub alpha_density: DMatrix<f64>,
pub beta_density: DMatrix<f64>,
pub total_density: DMatrix<f64>,
pub electronic_energy: f64,
pub nuclear_repulsion: f64,
pub total_energy: f64,
pub homo_energy: f64,
pub lumo_energy: Option<f64>,
pub gap_ev: f64,
pub mulliken_charges: Vec<f64>,
pub scf_iterations: usize,
pub converged: bool,
pub n_basis: usize,
pub n_alpha: usize,
pub n_beta: usize,
pub s2_expectation: f64,
pub spin_contamination: f64,
pub overlap_matrix: DMatrix<f64>,
pub alpha_fock: DMatrix<f64>,
pub beta_fock: DMatrix<f64>,
}
#[derive(Debug, Clone)]
pub struct UhfConfig {
pub max_iterations: usize,
pub energy_threshold: f64,
pub density_threshold: f64,
pub diis_size: usize,
pub level_shift: f64,
pub damping: f64,
pub use_parallel_eri: bool,
pub rohf: bool,
}
impl Default for UhfConfig {
fn default() -> Self {
Self {
max_iterations: SCF_MAX_ITER,
energy_threshold: SCF_ENERGY_THRESHOLD,
density_threshold: SCF_DENSITY_THRESHOLD,
diis_size: DIIS_SUBSPACE_SIZE,
level_shift: 0.0,
damping: 0.0,
use_parallel_eri: false,
rohf: false,
}
}
}
fn build_spin_density(coefficients: &DMatrix<f64>, n_occ: usize) -> DMatrix<f64> {
let n = coefficients.nrows();
let mut p = DMatrix::zeros(n, n);
for i in 0..n {
for j in 0..=i {
let mut val = 0.0;
for k in 0..n_occ {
val += coefficients[(i, k)] * coefficients[(j, k)];
}
p[(i, j)] = val;
p[(j, i)] = val;
}
}
p
}
fn build_uhf_fock(
h_core: &DMatrix<f64>,
p_total: &DMatrix<f64>,
p_spin: &DMatrix<f64>,
eris: &TwoElectronIntegrals,
) -> DMatrix<f64> {
let n = h_core.nrows();
let mut fock = h_core.clone();
for mu in 0..n {
for nu in 0..=mu {
let mut g = 0.0;
for lam in 0..n {
for sig in 0..n {
let j = p_total[(lam, sig)] * eris.get(mu, nu, lam, sig);
let k = p_spin[(lam, sig)] * eris.get(mu, lam, nu, sig);
g += j - k;
}
}
fock[(mu, nu)] += g;
if mu != nu {
fock[(nu, mu)] += g;
}
}
}
fock
}
fn uhf_electronic_energy(
p_alpha: &DMatrix<f64>,
p_beta: &DMatrix<f64>,
h_core: &DMatrix<f64>,
f_alpha: &DMatrix<f64>,
f_beta: &DMatrix<f64>,
) -> f64 {
let n = h_core.nrows();
let mut e = 0.0;
for i in 0..n {
for j in 0..n {
let p_t = p_alpha[(i, j)] + p_beta[(i, j)];
e += p_t * h_core[(i, j)]
+ p_alpha[(i, j)] * f_alpha[(i, j)]
+ p_beta[(i, j)] * f_beta[(i, j)];
}
}
0.5 * e
}
fn compute_s2(
c_alpha: &DMatrix<f64>,
c_beta: &DMatrix<f64>,
overlap: &DMatrix<f64>,
n_alpha: usize,
n_beta: usize,
) -> f64 {
let sz = 0.5 * (n_alpha as f64 - n_beta as f64);
let mut overlap_sum = 0.0;
for i in 0..n_alpha {
for j in 0..n_beta {
let mut s_ij = 0.0;
let n = overlap.nrows();
for mu in 0..n {
for nu in 0..n {
s_ij += c_alpha[(mu, i)] * overlap[(mu, nu)] * c_beta[(nu, j)];
}
}
overlap_sum += s_ij * s_ij;
}
}
sz * (sz + 1.0) + n_beta as f64 - overlap_sum
}
fn density_rms(a: &DMatrix<f64>, b: &DMatrix<f64>) -> f64 {
let n = a.nrows();
let mut sum = 0.0;
for i in 0..n {
for j in 0..n {
let d = a[(i, j)] - b[(i, j)];
sum += d * d;
}
}
(sum / (n * n) as f64).sqrt()
}
fn diag_fock(fock: &DMatrix<f64>, x: &DMatrix<f64>, n_basis: usize) -> (DMatrix<f64>, Vec<f64>) {
let f_ortho = transform_to_orthogonal(fock, x);
let eigen = f_ortho.symmetric_eigen();
let mut indices: Vec<usize> = (0..n_basis).collect();
indices.sort_by(|&a, &b| {
eigen.eigenvalues[a]
.partial_cmp(&eigen.eigenvalues[b])
.unwrap()
});
let mut c_ortho = DMatrix::zeros(n_basis, n_basis);
let mut energies = vec![0.0; n_basis];
for (new_idx, &old_idx) in indices.iter().enumerate() {
energies[new_idx] = eigen.eigenvalues[old_idx];
for i in 0..n_basis {
c_ortho[(i, new_idx)] = eigen.eigenvectors[(i, old_idx)];
}
}
let c = back_transform(&c_ortho, x);
(c, energies)
}
pub fn run_uhf(system: &MolecularSystem, config: &UhfConfig) -> UhfResult {
let n_electrons = system.n_electrons();
let n_unpaired = (system.multiplicity as usize).saturating_sub(1);
let n_alpha = (n_electrons + n_unpaired) / 2;
let n_beta = n_electrons - n_alpha;
let basis = BasisSet::sto3g(&system.atomic_numbers, &system.positions_bohr);
let n_basis = basis.n_basis;
let core = CoreMatrices::build(&basis, &system.atomic_numbers, &system.positions_bohr);
let s = &core.overlap;
let h_core = &core.core_hamiltonian;
let e_nuc = nuclear_repulsion_energy(&system.atomic_numbers, &system.positions_bohr);
let eris = {
#[cfg(feature = "parallel")]
{
if config.use_parallel_eri {
TwoElectronIntegrals::compute_parallel(&basis)
} else {
TwoElectronIntegrals::compute(&basis)
}
}
#[cfg(not(feature = "parallel"))]
{
TwoElectronIntegrals::compute(&basis)
}
};
let (x, _) = lowdin_orthogonalization(s, 1e-8);
let (mut c_alpha, mut e_alpha) = diag_fock(h_core, &x, n_basis);
let (mut c_beta, mut e_beta) = diag_fock(h_core, &x, n_basis);
let mut p_alpha = build_spin_density(&c_alpha, n_alpha);
let mut p_beta = build_spin_density(&c_beta, n_beta);
let mut p_total = &p_alpha + &p_beta;
let mut p_alpha_old = p_alpha.clone();
let mut p_beta_old = p_beta.clone();
let mut diis_alpha = DiisAccelerator::new(config.diis_size);
let mut diis_beta = DiisAccelerator::new(config.diis_size);
let mut e_total = 0.0;
let mut converged = false;
let mut scf_iter = 0;
let mut f_alpha = h_core.clone();
let mut f_beta = h_core.clone();
for iteration in 0..config.max_iterations {
scf_iter = iteration + 1;
f_alpha = build_uhf_fock(h_core, &p_total, &p_alpha, &eris);
f_beta = build_uhf_fock(h_core, &p_total, &p_beta, &eris);
if config.level_shift > 0.0 {
for i in 0..n_basis {
f_alpha[(i, i)] += config.level_shift;
f_beta[(i, i)] += config.level_shift;
}
}
if config.diis_size > 0 {
diis_alpha.add_iteration(&f_alpha, &p_alpha, s);
diis_beta.add_iteration(&f_beta, &p_beta, s);
if let Some(fa) = diis_alpha.extrapolate() {
f_alpha = fa;
}
if let Some(fb) = diis_beta.extrapolate() {
f_beta = fb;
}
}
let (ca_new, ea_new) = diag_fock(&f_alpha, &x, n_basis);
let (cb_new, eb_new) = diag_fock(&f_beta, &x, n_basis);
c_alpha = ca_new;
c_beta = cb_new;
e_alpha = ea_new;
e_beta = eb_new;
let pa_new = build_spin_density(&c_alpha, n_alpha);
let pb_new = build_spin_density(&c_beta, n_beta);
if config.damping > 0.0 {
p_alpha = &pa_new * (1.0 - config.damping) + &p_alpha_old * config.damping;
p_beta = &pb_new * (1.0 - config.damping) + &p_beta_old * config.damping;
} else {
p_alpha = pa_new;
p_beta = pb_new;
}
p_total = &p_alpha + &p_beta;
let e_elec = uhf_electronic_energy(&p_alpha, &p_beta, h_core, &f_alpha, &f_beta);
let e_new = e_elec + e_nuc;
let delta_e = (e_new - e_total).abs();
let delta_pa = density_rms(&p_alpha, &p_alpha_old);
let delta_pb = density_rms(&p_beta, &p_beta_old);
let delta_p = delta_pa.max(delta_pb);
if delta_e < config.energy_threshold && delta_p < config.density_threshold && iteration > 0
{
converged = true;
e_total = e_new;
break;
}
e_total = e_new;
p_alpha_old = p_alpha.clone();
p_beta_old = p_beta.clone();
}
if config.rohf {
let f_eff = (&f_alpha + &f_beta) * 0.5;
let (c_rohf, e_rohf) = diag_fock(&f_eff, &x, n_basis);
p_alpha = build_spin_density(&c_rohf, n_alpha);
p_beta = build_spin_density(&c_rohf, n_beta);
p_total = &p_alpha + &p_beta;
c_alpha = c_rohf.clone();
c_beta = c_rohf;
e_alpha = e_rohf.clone();
e_beta = e_rohf;
}
let e_elec = uhf_electronic_energy(&p_alpha, &p_beta, h_core, &f_alpha, &f_beta);
let homo_a = if n_alpha > 0 {
e_alpha[n_alpha - 1]
} else {
f64::NEG_INFINITY
};
let homo_b = if n_beta > 0 {
e_beta[n_beta - 1]
} else {
f64::NEG_INFINITY
};
let homo = homo_a.max(homo_b);
let lumo_a = if n_alpha < n_basis {
Some(e_alpha[n_alpha])
} else {
None
};
let lumo_b = if n_beta < n_basis {
Some(e_beta[n_beta])
} else {
None
};
let lumo = match (lumo_a, lumo_b) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
_ => None,
};
let gap = lumo.map(|l| (l - homo) * HARTREE_TO_EV).unwrap_or(0.0);
let s2 = compute_s2(&c_alpha, &c_beta, s, n_alpha, n_beta);
let s_exact = 0.5 * n_unpaired as f64;
let s2_exact = s_exact * (s_exact + 1.0);
let contamination = s2 - s2_exact;
let mulliken = super::mulliken::mulliken_analysis(
&p_total,
s,
&basis.function_to_atom,
&system.atomic_numbers,
);
UhfResult {
alpha_orbital_energies: e_alpha,
beta_orbital_energies: e_beta,
alpha_coefficients: c_alpha,
beta_coefficients: c_beta,
alpha_density: p_alpha,
beta_density: p_beta,
total_density: p_total,
electronic_energy: e_elec,
nuclear_repulsion: e_nuc,
total_energy: e_total,
homo_energy: homo,
lumo_energy: lumo,
gap_ev: gap,
mulliken_charges: mulliken.charges,
scf_iterations: scf_iter,
converged,
n_basis,
n_alpha,
n_beta,
s2_expectation: s2,
spin_contamination: contamination,
overlap_matrix: s.clone(),
alpha_fock: f_alpha,
beta_fock: f_beta,
}
}
pub fn run_uhf_parallel(system: &MolecularSystem) -> UhfResult {
run_uhf(
system,
&UhfConfig {
use_parallel_eri: true,
..UhfConfig::default()
},
)
}
pub fn run_rohf(system: &MolecularSystem) -> UhfResult {
run_uhf(
system,
&UhfConfig {
rohf: true,
use_parallel_eri: true,
..UhfConfig::default()
},
)
}