use super::fock::{build_fock, electronic_energy};
use nalgebra::{DMatrix, DVector, SymmetricEigen};
#[cfg(feature = "experimental-gpu")]
fn matrix_to_row_major(matrix: &DMatrix<f64>) -> Vec<f64> {
let nrows = matrix.nrows();
let ncols = matrix.ncols();
let mut data = Vec::with_capacity(nrows * ncols);
for row in 0..nrows {
for col in 0..ncols {
data.push(matrix[(row, col)]);
}
}
data
}
#[cfg(feature = "experimental-gpu")]
fn expand_packed_eris(eris: &[f64], n_basis: usize) -> Vec<f64> {
let mut full = vec![0.0; n_basis * n_basis * n_basis * n_basis];
for mu in 0..n_basis {
for nu in 0..n_basis {
for lam in 0..n_basis {
for sig in 0..n_basis {
let packed = super::integrals::get_eri(eris, mu, nu, lam, sig, n_basis);
let idx = mu * n_basis * n_basis * n_basis
+ nu * n_basis * n_basis
+ lam * n_basis
+ sig;
full[idx] = packed;
}
}
}
}
full
}
#[derive(Debug, Clone)]
pub struct ScfResult {
pub energy: f64,
pub orbital_energies: Vec<f64>,
pub coefficients: DMatrix<f64>,
pub density: DMatrix<f64>,
pub iterations: usize,
pub converged: bool,
}
pub struct ScfConfig {
pub max_iter: usize,
pub energy_threshold: f64,
pub density_threshold: f64,
pub diis_size: usize,
pub level_shift: f64,
}
impl Default for ScfConfig {
fn default() -> Self {
ScfConfig {
max_iter: 100,
energy_threshold: 1e-8,
density_threshold: 1e-6,
diis_size: 6,
level_shift: 0.0,
}
}
}
pub fn solve_scf(
h_core: &DMatrix<f64>,
s_mat: &DMatrix<f64>,
eris: &[f64],
_gpu_eris_full: Option<&[f64]>,
n_electrons: usize,
config: &ScfConfig,
) -> ScfResult {
let n = h_core.nrows();
let n_occ = n_electrons / 2;
#[cfg(feature = "experimental-gpu")]
let gpu_ctx = if n >= 4 {
crate::gpu::context::GpuContext::try_create().ok()
} else {
None
};
#[cfg(feature = "experimental-gpu")]
let h_core_row_major = matrix_to_row_major(h_core);
#[cfg(feature = "experimental-gpu")]
let eris_full = _gpu_eris_full
.map(|tensor| tensor.to_vec())
.or_else(|| gpu_ctx.as_ref().map(|_| expand_packed_eris(eris, n)));
let s_half_inv = lowdin_orthogonalization(s_mat);
let (mut energies, mut coeffs) = diagonalize_fock(h_core, &s_half_inv);
let mut density = build_density(&coeffs, n_occ);
let mut prev_energy = 0.0;
let mut converged = false;
let mut iterations = 0;
let mut diis_focks: Vec<DMatrix<f64>> = Vec::new();
let mut diis_errors: Vec<DMatrix<f64>> = Vec::new();
let mut min_error_norm = f64::MAX;
let mut consecutive_increases = 0u32;
for iter in 0..config.max_iter {
iterations = iter + 1;
let fock = {
#[cfg(feature = "experimental-gpu")]
{
if let (Some(ctx), Some(eri_tensor)) = (gpu_ctx.as_ref(), eris_full.as_ref()) {
let density_row_major = matrix_to_row_major(&density);
if let Ok(fock_flat) = crate::gpu::fock_build_gpu::build_fock_gpu(
ctx,
&h_core_row_major,
&density_row_major,
eri_tensor,
n,
) {
DMatrix::from_row_slice(n, n, &fock_flat)
} else {
build_fock(h_core, &density, eris, n)
}
} else {
build_fock(h_core, &density, eris, n)
}
}
#[cfg(not(feature = "experimental-gpu"))]
{
build_fock(h_core, &density, eris, n)
}
};
let energy = electronic_energy(&density, h_core, &fock);
let error = &fock * &density * s_mat - s_mat * &density * &fock;
let error_norm = error.iter().map(|v| v * v).sum::<f64>().sqrt();
if error_norm > min_error_norm * 1.5 {
consecutive_increases += 1;
} else {
consecutive_increases = 0;
}
if consecutive_increases >= 3 && diis_focks.len() > 2 {
diis_focks.clear();
diis_errors.clear();
consecutive_increases = 0;
}
if error_norm < min_error_norm {
min_error_norm = error_norm;
}
diis_focks.push(fock.clone());
diis_errors.push(error);
if diis_focks.len() > config.diis_size {
diis_focks.remove(0);
diis_errors.remove(0);
}
let fock_diis = if diis_focks.len() >= 2 {
diis_extrapolate(&diis_focks, &diis_errors)
} else {
fock
};
let fock_shifted = if config.level_shift > 0.0 && n_occ < n {
let f_orth = s_half_inv.transpose() * &fock_diis * &s_half_inv;
let eigen = SymmetricEigen::new(f_orth);
let mut shifted_evals = eigen.eigenvalues.clone();
let mut idx_sorted: Vec<usize> = (0..n).collect();
idx_sorted.sort_by(|&a, &b| shifted_evals[a].partial_cmp(&shifted_evals[b]).unwrap());
for &idx in idx_sorted.iter().skip(n_occ) {
shifted_evals[idx] += config.level_shift;
}
let v = &eigen.eigenvectors;
let d = DMatrix::from_diagonal(&shifted_evals);
let f_shifted = v * d * v.transpose();
&s_half_inv * f_shifted * s_half_inv.transpose()
} else {
fock_diis
};
let (new_energies, new_coeffs) = diagonalize_fock(&fock_shifted, &s_half_inv);
let new_density = build_density(&new_coeffs, n_occ);
let de = (energy - prev_energy).abs();
if de < config.energy_threshold && error_norm < config.density_threshold {
converged = true;
energies = new_energies;
coeffs = new_coeffs;
density = new_density;
break;
}
prev_energy = energy;
energies = new_energies;
coeffs = new_coeffs;
density = new_density;
}
let final_energy = electronic_energy(&density, h_core, &build_fock(h_core, &density, eris, n));
ScfResult {
energy: final_energy,
orbital_energies: energies.as_slice().to_vec(),
coefficients: coeffs,
density,
iterations,
converged,
}
}
fn lowdin_orthogonalization(s: &DMatrix<f64>) -> DMatrix<f64> {
let eigen = SymmetricEigen::new(s.clone());
let n = s.nrows();
let mut s_inv_half = DMatrix::zeros(n, n);
let max_val = eigen
.eigenvalues
.iter()
.copied()
.fold(0.0f64, |a, b| a.max(b));
let threshold = max_val * 1e-10;
for i in 0..n {
let val = eigen.eigenvalues[i];
if val > threshold {
let factor = 1.0 / val.sqrt();
let col = eigen.eigenvectors.column(i);
s_inv_half += factor * col * col.transpose();
}
}
s_inv_half
}
fn diagonalize_fock(
fock: &DMatrix<f64>,
s_half_inv: &DMatrix<f64>,
) -> (DVector<f64>, DMatrix<f64>) {
let f_prime = s_half_inv.transpose() * fock * s_half_inv;
let eigen = SymmetricEigen::new(f_prime);
let n = eigen.eigenvalues.len();
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&a, &b| {
eigen.eigenvalues[a]
.partial_cmp(&eigen.eigenvalues[b])
.unwrap()
});
let sorted_energies = DVector::from_fn(n, |i, _| eigen.eigenvalues[indices[i]]);
let sorted_vecs = DMatrix::from_fn(n, n, |r, c| eigen.eigenvectors[(r, indices[c])]);
let coeffs = s_half_inv * sorted_vecs;
(sorted_energies, coeffs)
}
fn build_density(coeffs: &DMatrix<f64>, n_occ: usize) -> DMatrix<f64> {
let n = coeffs.nrows();
let mut density = DMatrix::zeros(n, n);
for i in 0..n_occ {
let col = coeffs.column(i);
density += 2.0 * &col * col.transpose();
}
density
}
fn diis_extrapolate(focks: &[DMatrix<f64>], errors: &[DMatrix<f64>]) -> DMatrix<f64> {
let m = errors.len();
let mut b = DMatrix::zeros(m + 1, m + 1);
for i in 0..m {
for j in 0..=i {
let bij: f64 = errors[i]
.iter()
.zip(errors[j].iter())
.map(|(a, b)| a * b)
.sum();
b[(i, j)] = bij;
b[(j, i)] = bij;
}
}
for i in 0..m {
b[(m, i)] = -1.0;
b[(i, m)] = -1.0;
}
let mut rhs = DVector::zeros(m + 1);
rhs[m] = -1.0;
let svd = b.svd(true, true);
let c = match svd.solve(&rhs, 1e-10) {
Ok(c) => c,
Err(_) => {
return focks.last().unwrap().clone();
}
};
let max_coeff = c.iter().take(m).map(|v| v.abs()).fold(0.0f64, f64::max);
if max_coeff > 10.0 {
return focks.last().unwrap().clone();
}
let mut f_diis = DMatrix::zeros(focks[0].nrows(), focks[0].ncols());
for i in 0..m {
f_diis += c[i] * &focks[i];
}
f_diis
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lowdin_identity() {
let s = DMatrix::identity(3, 3);
let s_inv = lowdin_orthogonalization(&s);
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(s_inv[(i, j)] - expected).abs() < 1e-10,
"S^{{-1/2}} of identity should be identity"
);
}
}
}
}