1use super::fock::{build_fock, electronic_energy};
10use nalgebra::{DMatrix, DVector, SymmetricEigen};
11
12#[cfg(feature = "experimental-gpu")]
13fn matrix_to_row_major(matrix: &DMatrix<f64>) -> Vec<f64> {
14 let nrows = matrix.nrows();
15 let ncols = matrix.ncols();
16 let mut data = Vec::with_capacity(nrows * ncols);
17 for row in 0..nrows {
18 for col in 0..ncols {
19 data.push(matrix[(row, col)]);
20 }
21 }
22 data
23}
24
25#[cfg(feature = "experimental-gpu")]
26fn expand_packed_eris(eris: &[f64], n_basis: usize) -> Vec<f64> {
27 let mut full = vec![0.0; n_basis * n_basis * n_basis * n_basis];
28 for mu in 0..n_basis {
29 for nu in 0..n_basis {
30 for lam in 0..n_basis {
31 for sig in 0..n_basis {
32 let packed = super::integrals::get_eri(eris, mu, nu, lam, sig, n_basis);
33 let idx = mu * n_basis * n_basis * n_basis
34 + nu * n_basis * n_basis
35 + lam * n_basis
36 + sig;
37 full[idx] = packed;
38 }
39 }
40 }
41 }
42 full
43}
44
45#[derive(Debug, Clone)]
51pub struct ScfResult {
52 pub energy: f64,
54 pub orbital_energies: Vec<f64>,
56 pub coefficients: DMatrix<f64>,
58 pub density: DMatrix<f64>,
60 pub iterations: usize,
62 pub converged: bool,
64}
65
66pub struct ScfConfig {
68 pub max_iter: usize,
69 pub energy_threshold: f64,
70 pub density_threshold: f64,
71 pub diis_size: usize,
72 pub level_shift: f64,
75}
76
77impl Default for ScfConfig {
78 fn default() -> Self {
79 ScfConfig {
80 max_iter: 100,
81 energy_threshold: 1e-8,
82 density_threshold: 1e-6,
83 diis_size: 6,
84 level_shift: 0.0,
85 }
86 }
87}
88
89pub fn solve_scf(
91 h_core: &DMatrix<f64>,
92 s_mat: &DMatrix<f64>,
93 eris: &[f64],
94 _gpu_eris_full: Option<&[f64]>,
95 n_electrons: usize,
96 config: &ScfConfig,
97) -> ScfResult {
98 let n = h_core.nrows();
99 let n_occ = n_electrons / 2;
100
101 #[cfg(feature = "experimental-gpu")]
102 let gpu_ctx = if n >= 4 {
103 crate::gpu::context::GpuContext::try_create().ok()
104 } else {
105 None
106 };
107
108 #[cfg(feature = "experimental-gpu")]
109 let h_core_row_major = matrix_to_row_major(h_core);
110
111 #[cfg(feature = "experimental-gpu")]
112 let eris_full = _gpu_eris_full
113 .map(|tensor| tensor.to_vec())
114 .or_else(|| gpu_ctx.as_ref().map(|_| expand_packed_eris(eris, n)));
115
116 let s_half_inv = lowdin_orthogonalization(s_mat);
118
119 let (mut energies, mut coeffs) = diagonalize_fock(h_core, &s_half_inv);
121 let mut density = build_density(&coeffs, n_occ);
122
123 let mut prev_energy = 0.0;
124 let mut converged = false;
125 let mut iterations = 0;
126
127 let mut diis_focks: Vec<DMatrix<f64>> = Vec::new();
129 let mut diis_errors: Vec<DMatrix<f64>> = Vec::new();
130
131 let mut min_error_norm = f64::MAX;
132 let mut consecutive_increases = 0u32;
133
134 for iter in 0..config.max_iter {
135 iterations = iter + 1;
136
137 let fock = {
138 #[cfg(feature = "experimental-gpu")]
139 {
140 if let (Some(ctx), Some(eri_tensor)) = (gpu_ctx.as_ref(), eris_full.as_ref()) {
141 let density_row_major = matrix_to_row_major(&density);
142 if let Ok(fock_flat) = crate::gpu::fock_build_gpu::build_fock_gpu(
143 ctx,
144 &h_core_row_major,
145 &density_row_major,
146 eri_tensor,
147 n,
148 ) {
149 DMatrix::from_row_slice(n, n, &fock_flat)
150 } else {
151 build_fock(h_core, &density, eris, n)
152 }
153 } else {
154 build_fock(h_core, &density, eris, n)
155 }
156 }
157
158 #[cfg(not(feature = "experimental-gpu"))]
159 {
160 build_fock(h_core, &density, eris, n)
161 }
162 };
163 let energy = electronic_energy(&density, h_core, &fock);
164
165 let error = &fock * &density * s_mat - s_mat * &density * &fock;
167 let error_norm = error.iter().map(|v| v * v).sum::<f64>().sqrt();
168
169 if error_norm > min_error_norm * 1.5 {
172 consecutive_increases += 1;
173 } else {
174 consecutive_increases = 0;
175 }
176 if consecutive_increases >= 3 && diis_focks.len() > 2 {
177 diis_focks.clear();
178 diis_errors.clear();
179 consecutive_increases = 0;
180 }
181 if error_norm < min_error_norm {
182 min_error_norm = error_norm;
183 }
184
185 diis_focks.push(fock.clone());
187 diis_errors.push(error);
188 if diis_focks.len() > config.diis_size {
189 diis_focks.remove(0);
190 diis_errors.remove(0);
191 }
192
193 let fock_diis = if diis_focks.len() >= 2 {
194 diis_extrapolate(&diis_focks, &diis_errors)
195 } else {
196 fock
197 };
198
199 let fock_shifted = if config.level_shift > 0.0 && n_occ < n {
201 let f_orth = s_half_inv.transpose() * &fock_diis * &s_half_inv;
204 let eigen = SymmetricEigen::new(f_orth);
205 let mut shifted_evals = eigen.eigenvalues.clone();
206 let mut idx_sorted: Vec<usize> = (0..n).collect();
207 idx_sorted.sort_by(|&a, &b| shifted_evals[a].partial_cmp(&shifted_evals[b]).unwrap());
208 for &idx in idx_sorted.iter().skip(n_occ) {
209 shifted_evals[idx] += config.level_shift;
210 }
211 let v = &eigen.eigenvectors;
212 let d = DMatrix::from_diagonal(&shifted_evals);
213 let f_shifted = v * d * v.transpose();
214 &s_half_inv * f_shifted * s_half_inv.transpose()
217 } else {
218 fock_diis
219 };
220
221 let (new_energies, new_coeffs) = diagonalize_fock(&fock_shifted, &s_half_inv);
222 let new_density = build_density(&new_coeffs, n_occ);
223
224 let de = (energy - prev_energy).abs();
225
226 if de < config.energy_threshold && error_norm < config.density_threshold {
227 converged = true;
228 energies = new_energies;
229 coeffs = new_coeffs;
230 density = new_density;
231 break;
232 }
233
234 prev_energy = energy;
235 energies = new_energies;
236 coeffs = new_coeffs;
237 density = new_density;
238 }
239
240 let final_energy = electronic_energy(&density, h_core, &build_fock(h_core, &density, eris, n));
241
242 ScfResult {
243 energy: final_energy,
244 orbital_energies: energies.as_slice().to_vec(),
245 coefficients: coeffs,
246 density,
247 iterations,
248 converged,
249 }
250}
251
252fn lowdin_orthogonalization(s: &DMatrix<f64>) -> DMatrix<f64> {
253 let eigen = SymmetricEigen::new(s.clone());
254 let n = s.nrows();
255 let mut s_inv_half = DMatrix::zeros(n, n);
256
257 let max_val = eigen
258 .eigenvalues
259 .iter()
260 .copied()
261 .fold(0.0f64, |a, b| a.max(b));
262 let threshold = max_val * 1e-10;
263
264 for i in 0..n {
265 let val = eigen.eigenvalues[i];
266 if val > threshold {
267 let factor = 1.0 / val.sqrt();
268 let col = eigen.eigenvectors.column(i);
269 s_inv_half += factor * col * col.transpose();
270 }
271 }
272 s_inv_half
273}
274
275fn diagonalize_fock(
276 fock: &DMatrix<f64>,
277 s_half_inv: &DMatrix<f64>,
278) -> (DVector<f64>, DMatrix<f64>) {
279 let f_prime = s_half_inv.transpose() * fock * s_half_inv;
280 let eigen = SymmetricEigen::new(f_prime);
281
282 let n = eigen.eigenvalues.len();
284 let mut indices: Vec<usize> = (0..n).collect();
285 indices.sort_by(|&a, &b| {
286 eigen.eigenvalues[a]
287 .partial_cmp(&eigen.eigenvalues[b])
288 .unwrap()
289 });
290
291 let sorted_energies = DVector::from_fn(n, |i, _| eigen.eigenvalues[indices[i]]);
292 let sorted_vecs = DMatrix::from_fn(n, n, |r, c| eigen.eigenvectors[(r, indices[c])]);
293
294 let coeffs = s_half_inv * sorted_vecs;
296 (sorted_energies, coeffs)
297}
298
299fn build_density(coeffs: &DMatrix<f64>, n_occ: usize) -> DMatrix<f64> {
300 let n = coeffs.nrows();
301 let mut density = DMatrix::zeros(n, n);
302 for i in 0..n_occ {
303 let col = coeffs.column(i);
304 density += 2.0 * &col * col.transpose();
305 }
306 density
307}
308
309fn diis_extrapolate(focks: &[DMatrix<f64>], errors: &[DMatrix<f64>]) -> DMatrix<f64> {
310 let m = errors.len();
311 let mut b = DMatrix::zeros(m + 1, m + 1);
312
313 for i in 0..m {
314 for j in 0..=i {
315 let bij: f64 = errors[i]
316 .iter()
317 .zip(errors[j].iter())
318 .map(|(a, b)| a * b)
319 .sum();
320 b[(i, j)] = bij;
321 b[(j, i)] = bij;
322 }
323 }
324 for i in 0..m {
325 b[(m, i)] = -1.0;
326 b[(i, m)] = -1.0;
327 }
328
329 let mut rhs = DVector::zeros(m + 1);
330 rhs[m] = -1.0;
331
332 let svd = b.svd(true, true);
334 let c = match svd.solve(&rhs, 1e-10) {
335 Ok(c) => c,
336 Err(_) => {
337 return focks.last().unwrap().clone();
339 }
340 };
341
342 let max_coeff = c.iter().take(m).map(|v| v.abs()).fold(0.0f64, f64::max);
345 if max_coeff > 10.0 {
346 return focks.last().unwrap().clone();
347 }
348
349 let mut f_diis = DMatrix::zeros(focks[0].nrows(), focks[0].ncols());
350 for i in 0..m {
351 f_diis += c[i] * &focks[i];
352 }
353 f_diis
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_lowdin_identity() {
362 let s = DMatrix::identity(3, 3);
363 let s_inv = lowdin_orthogonalization(&s);
364 for i in 0..3 {
365 for j in 0..3 {
366 let expected = if i == j { 1.0 } else { 0.0 };
367 assert!(
368 (s_inv[(i, j)] - expected).abs() < 1e-10,
369 "S^{{-1/2}} of identity should be identity"
370 );
371 }
372 }
373 }
374}