1use nalgebra::DMatrix;
12
13use super::basis::BasisSet;
14use super::constants::{
15 DIIS_SUBSPACE_SIZE, HARTREE_TO_EV, SCF_DENSITY_THRESHOLD, SCF_ENERGY_THRESHOLD, SCF_MAX_ITER,
16};
17use super::core_matrices::{nuclear_repulsion_energy, CoreMatrices};
18use super::density_matrix::{build_density_matrix, density_rms_change};
19use super::diis::DiisAccelerator;
20use super::energy;
21use super::fock_matrix::build_fock_matrix;
22use super::mulliken::mulliken_analysis;
23use super::orthogonalization::{back_transform, lowdin_orthogonalization, transform_to_orthogonal};
24use super::two_electron::TwoElectronIntegrals;
25use super::types::{MolecularSystem, ScfResult};
26
27#[derive(Debug, Clone)]
29pub struct ScfConfig {
30 pub max_iterations: usize,
32 pub energy_threshold: f64,
34 pub density_threshold: f64,
36 pub diis_size: usize,
38 pub level_shift: f64,
40 pub damping: f64,
42 pub use_parallel_eri: bool,
44 pub parallel_threshold: usize,
46}
47
48impl Default for ScfConfig {
49 fn default() -> Self {
50 Self {
51 max_iterations: SCF_MAX_ITER,
52 energy_threshold: SCF_ENERGY_THRESHOLD,
53 density_threshold: SCF_DENSITY_THRESHOLD,
54 diis_size: DIIS_SUBSPACE_SIZE,
55 level_shift: 0.0,
56 damping: 0.0,
57 use_parallel_eri: false,
58 parallel_threshold: 20,
59 }
60 }
61}
62
63impl ScfConfig {
64 pub fn parallel() -> Self {
66 Self {
67 use_parallel_eri: true,
68 parallel_threshold: 0,
69 ..Self::default()
70 }
71 }
72}
73
74pub fn run_scf(system: &MolecularSystem, config: &ScfConfig) -> ScfResult {
76 let n_electrons = system.n_electrons();
77 let n_occupied = n_electrons / 2;
78
79 let basis = BasisSet::sto3g(&system.atomic_numbers, &system.positions_bohr);
81 let n_basis = basis.n_basis;
82
83 let core_matrices = CoreMatrices::build(&basis, &system.atomic_numbers, &system.positions_bohr);
85 let s = &core_matrices.overlap;
86 let h_core = &core_matrices.core_hamiltonian;
87
88 let use_parallel = config.use_parallel_eri && basis.n_basis >= config.parallel_threshold;
90 let eris = if use_parallel {
91 #[cfg(feature = "parallel")]
92 {
93 TwoElectronIntegrals::compute_parallel(&basis)
94 }
95 #[cfg(not(feature = "parallel"))]
96 {
97 TwoElectronIntegrals::compute(&basis)
98 }
99 } else {
100 TwoElectronIntegrals::compute(&basis)
101 };
102
103 let e_nuc = nuclear_repulsion_energy(&system.atomic_numbers, &system.positions_bohr);
105
106 let (x, _n_independent) = lowdin_orthogonalization(s, 1e-8);
108
109 let h_ortho = transform_to_orthogonal(h_core, &x);
111 let eigen = h_ortho.symmetric_eigen();
112
113 let mut indices: Vec<usize> = (0..n_basis).collect();
114 indices.sort_by(|&a, &b| {
115 eigen.eigenvalues[a]
116 .partial_cmp(&eigen.eigenvalues[b])
117 .unwrap()
118 });
119
120 let mut c_ortho = DMatrix::zeros(n_basis, n_basis);
121 let mut orbital_energies = vec![0.0; n_basis];
122 for (new_idx, &old_idx) in indices.iter().enumerate() {
123 orbital_energies[new_idx] = eigen.eigenvalues[old_idx];
124 for i in 0..n_basis {
125 c_ortho[(i, new_idx)] = eigen.eigenvectors[(i, old_idx)];
126 }
127 }
128
129 let mut c = back_transform(&c_ortho, &x);
130 let mut p = build_density_matrix(&c, n_occupied);
131 let mut p_old = p.clone();
132
133 let mut diis = DiisAccelerator::new(config.diis_size);
135 let mut e_total = 0.0;
136 let mut converged = false;
137 let mut scf_iter = 0;
138 let mut fock = h_core.clone();
139
140 for iteration in 0..config.max_iterations {
141 scf_iter = iteration + 1;
142
143 fock = build_fock_matrix(h_core, &p, &eris);
144
145 if config.level_shift > 0.0 {
147 let n = fock.nrows();
148 for i in 0..n {
149 fock[(i, i)] += config.level_shift;
150 }
151 }
152
153 if config.diis_size > 0 {
155 diis.add_iteration(&fock, &p, s);
156 if let Some(f_diis) = diis.extrapolate() {
157 fock = f_diis;
158 }
159 }
160
161 let f_ortho = transform_to_orthogonal(&fock, &x);
163 let eigen = f_ortho.symmetric_eigen();
164
165 let mut indices: Vec<usize> = (0..n_basis).collect();
166 indices.sort_by(|&a, &b| {
167 eigen.eigenvalues[a]
168 .partial_cmp(&eigen.eigenvalues[b])
169 .unwrap()
170 });
171
172 for (new_idx, &old_idx) in indices.iter().enumerate() {
173 orbital_energies[new_idx] = eigen.eigenvalues[old_idx];
174 for i in 0..n_basis {
175 c_ortho[(i, new_idx)] = eigen.eigenvectors[(i, old_idx)];
176 }
177 }
178
179 c = back_transform(&c_ortho, &x);
180 let p_new = build_density_matrix(&c, n_occupied);
181
182 if config.damping > 0.0 {
183 p = &p_new * (1.0 - config.damping) + &p_old * config.damping;
184 } else {
185 p = p_new;
186 }
187
188 let e_new = energy::total_energy(&p, h_core, &fock, e_nuc);
189 let delta_e = (e_new - e_total).abs();
190 let delta_p = density_rms_change(&p, &p_old);
191
192 if delta_e < config.energy_threshold && delta_p < config.density_threshold && iteration > 0
193 {
194 converged = true;
195 e_total = e_new;
196 break;
197 }
198
199 e_total = e_new;
200 p_old = p.clone();
201 }
202
203 let e_elec = energy::electronic_energy(&p, h_core, &fock);
204
205 let homo_energy = if n_occupied > 0 {
206 orbital_energies[n_occupied - 1]
207 } else {
208 0.0
209 };
210 let lumo_energy = if n_occupied < n_basis {
211 Some(orbital_energies[n_occupied])
212 } else {
213 None
214 };
215 let gap_ev = lumo_energy
216 .map(|lumo| (lumo - homo_energy) * HARTREE_TO_EV)
217 .unwrap_or(0.0);
218
219 let mulliken = mulliken_analysis(&p, s, &basis.function_to_atom, &system.atomic_numbers);
220
221 ScfResult {
222 orbital_energies,
223 mo_coefficients: c,
224 density_matrix: p,
225 electronic_energy: e_elec,
226 nuclear_repulsion: e_nuc,
227 total_energy: e_total,
228 homo_energy,
229 lumo_energy,
230 gap_ev,
231 mulliken_charges: mulliken.charges,
232 scf_iterations: scf_iter,
233 converged,
234 n_basis,
235 n_electrons,
236 overlap_matrix: s.clone(),
237 fock_matrix: fock,
238 }
239}
240
241pub fn run_scf_hardened(system: &MolecularSystem, config: &ScfConfig) -> ScfResult {
247 let result = run_scf(system, config);
249 if result.converged {
250 return result;
251 }
252
253 let config_ls = ScfConfig {
255 level_shift: 0.3,
256 damping: 0.3,
257 ..config.clone()
258 };
259 let result_ls = run_scf(system, &config_ls);
260 if result_ls.converged {
261 return result_ls;
262 }
263
264 run_scf_with_fon(system, config)
266}
267
268fn run_scf_with_fon(system: &MolecularSystem, config: &ScfConfig) -> ScfResult {
270 let n_electrons = system.n_electrons();
271 let n_occupied = n_electrons / 2;
272 let temperature_au = 0.01; let basis = BasisSet::sto3g(&system.atomic_numbers, &system.positions_bohr);
275 let n_basis = basis.n_basis;
276
277 let core_matrices = CoreMatrices::build(&basis, &system.atomic_numbers, &system.positions_bohr);
278 let s = &core_matrices.overlap;
279 let h_core = &core_matrices.core_hamiltonian;
280
281 let eris = TwoElectronIntegrals::compute(&basis);
282 let e_nuc = nuclear_repulsion_energy(&system.atomic_numbers, &system.positions_bohr);
283
284 let (x, _n_independent) = lowdin_orthogonalization(s, 1e-8);
285
286 let h_ortho = transform_to_orthogonal(h_core, &x);
288 let eigen = h_ortho.symmetric_eigen();
289
290 let mut indices: Vec<usize> = (0..n_basis).collect();
291 indices.sort_by(|&a, &b| {
292 eigen.eigenvalues[a]
293 .partial_cmp(&eigen.eigenvalues[b])
294 .unwrap()
295 });
296
297 let mut c_ortho = DMatrix::zeros(n_basis, n_basis);
298 let mut orbital_energies = vec![0.0; n_basis];
299 for (new_idx, &old_idx) in indices.iter().enumerate() {
300 orbital_energies[new_idx] = eigen.eigenvalues[old_idx];
301 for i in 0..n_basis {
302 c_ortho[(i, new_idx)] = eigen.eigenvectors[(i, old_idx)];
303 }
304 }
305
306 let mut c = back_transform(&c_ortho, &x);
307 let mut p = super::density_matrix::build_density_matrix_fon(
308 &c,
309 &orbital_energies,
310 n_electrons,
311 temperature_au,
312 );
313 let mut p_old = p.clone();
314
315 let mut diis = DiisAccelerator::new(config.diis_size);
316 let mut e_total = 0.0;
317 let mut converged = false;
318 let mut scf_iter = 0;
319 let mut fock = h_core.clone();
320
321 let level_shift = 0.5; let damping = 0.4;
323
324 for iteration in 0..config.max_iterations {
325 scf_iter = iteration + 1;
326
327 fock = build_fock_matrix(h_core, &p, &eris);
328
329 for i in 0..fock.nrows() {
331 fock[(i, i)] += level_shift;
332 }
333
334 if config.diis_size > 0 {
336 diis.add_iteration(&fock, &p, s);
337 if let Some(f_diis) = diis.extrapolate() {
338 fock = f_diis;
339 }
340 }
341
342 let f_ortho = transform_to_orthogonal(&fock, &x);
343 let eigen = f_ortho.symmetric_eigen();
344
345 let mut indices: Vec<usize> = (0..n_basis).collect();
346 indices.sort_by(|&a, &b| {
347 eigen.eigenvalues[a]
348 .partial_cmp(&eigen.eigenvalues[b])
349 .unwrap()
350 });
351
352 for (new_idx, &old_idx) in indices.iter().enumerate() {
353 orbital_energies[new_idx] = eigen.eigenvalues[old_idx] - level_shift;
354 for i in 0..n_basis {
355 c_ortho[(i, new_idx)] = eigen.eigenvectors[(i, old_idx)];
356 }
357 }
358
359 c = back_transform(&c_ortho, &x);
360
361 let p_new = super::density_matrix::build_density_matrix_fon(
363 &c,
364 &orbital_energies,
365 n_electrons,
366 temperature_au,
367 );
368
369 p = &p_new * (1.0 - damping) + &p_old * damping;
370
371 let e_new = energy::total_energy(&p, h_core, &fock, e_nuc);
372 let delta_e = (e_new - e_total).abs();
373 let delta_p = super::density_matrix::density_rms_change(&p, &p_old);
374
375 if delta_e < config.energy_threshold && delta_p < config.density_threshold && iteration > 0
376 {
377 converged = true;
378 e_total = e_new;
379 break;
380 }
381
382 e_total = e_new;
383 p_old = p.clone();
384 }
385
386 let e_elec = energy::electronic_energy(&p, h_core, &fock);
387 let homo_energy = if n_occupied > 0 {
388 orbital_energies[n_occupied - 1]
389 } else {
390 0.0
391 };
392 let lumo_energy = if n_occupied < n_basis {
393 Some(orbital_energies[n_occupied])
394 } else {
395 None
396 };
397 let gap_ev = lumo_energy
398 .map(|lumo| (lumo - homo_energy) * super::constants::HARTREE_TO_EV)
399 .unwrap_or(0.0);
400
401 let mulliken = mulliken_analysis(&p, s, &basis.function_to_atom, &system.atomic_numbers);
402
403 ScfResult {
404 orbital_energies,
405 mo_coefficients: c,
406 density_matrix: p,
407 electronic_energy: e_elec,
408 nuclear_repulsion: e_nuc,
409 total_energy: e_total,
410 homo_energy,
411 lumo_energy,
412 gap_ev,
413 mulliken_charges: mulliken.charges,
414 scf_iterations: scf_iter,
415 converged,
416 n_basis,
417 n_electrons,
418 overlap_matrix: s.clone(),
419 fock_matrix: fock,
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::super::constants::ANGSTROM_TO_BOHR;
426 use super::*;
427
428 #[test]
429 fn test_scf_h2() {
430 let system = MolecularSystem {
431 atomic_numbers: vec![1, 1],
432 positions_bohr: vec![[0.0, 0.0, 0.0], [0.74 * ANGSTROM_TO_BOHR, 0.0, 0.0]],
433 charge: 0,
434 multiplicity: 1,
435 };
436
437 let config = ScfConfig::default();
438 let result = run_scf(&system, &config);
439
440 assert!(result.total_energy < 0.0, "Total energy should be negative");
441 assert_eq!(result.n_basis, 2);
442 assert_eq!(result.n_electrons, 2);
443 }
444
445 #[test]
446 fn test_scf_converges() {
447 let system = MolecularSystem {
448 atomic_numbers: vec![1, 1],
449 positions_bohr: vec![[0.0, 0.0, 0.0], [1.4, 0.0, 0.0]],
450 charge: 0,
451 multiplicity: 1,
452 };
453
454 let result = run_scf(&system, &ScfConfig::default());
455 assert!(result.converged, "SCF should converge for H2");
456 assert!(result.scf_iterations < 50);
457 }
458}