1use nalgebra::DMatrix;
13
14use super::basis::BasisSet;
15use super::constants::{
16 DIIS_SUBSPACE_SIZE, HARTREE_TO_EV, SCF_DENSITY_THRESHOLD, SCF_ENERGY_THRESHOLD, SCF_MAX_ITER,
17};
18use super::core_matrices::{nuclear_repulsion_energy, CoreMatrices};
19use super::diis::DiisAccelerator;
20use super::orthogonalization::{back_transform, lowdin_orthogonalization, transform_to_orthogonal};
21use super::two_electron::TwoElectronIntegrals;
22use super::types::MolecularSystem;
23
24#[derive(Debug, Clone)]
26pub struct UhfResult {
27 pub alpha_orbital_energies: Vec<f64>,
29 pub beta_orbital_energies: Vec<f64>,
31 pub alpha_coefficients: DMatrix<f64>,
33 pub beta_coefficients: DMatrix<f64>,
35 pub alpha_density: DMatrix<f64>,
37 pub beta_density: DMatrix<f64>,
39 pub total_density: DMatrix<f64>,
41 pub electronic_energy: f64,
43 pub nuclear_repulsion: f64,
45 pub total_energy: f64,
47 pub homo_energy: f64,
49 pub lumo_energy: Option<f64>,
51 pub gap_ev: f64,
53 pub mulliken_charges: Vec<f64>,
55 pub scf_iterations: usize,
57 pub converged: bool,
59 pub n_basis: usize,
61 pub n_alpha: usize,
63 pub n_beta: usize,
65 pub s2_expectation: f64,
67 pub spin_contamination: f64,
69 pub overlap_matrix: DMatrix<f64>,
71 pub alpha_fock: DMatrix<f64>,
73 pub beta_fock: DMatrix<f64>,
75}
76
77#[derive(Debug, Clone)]
79pub struct UhfConfig {
80 pub max_iterations: usize,
81 pub energy_threshold: f64,
82 pub density_threshold: f64,
83 pub diis_size: usize,
84 pub level_shift: f64,
85 pub damping: f64,
86 pub use_parallel_eri: bool,
87 pub rohf: bool,
89}
90
91impl Default for UhfConfig {
92 fn default() -> Self {
93 Self {
94 max_iterations: SCF_MAX_ITER,
95 energy_threshold: SCF_ENERGY_THRESHOLD,
96 density_threshold: SCF_DENSITY_THRESHOLD,
97 diis_size: DIIS_SUBSPACE_SIZE,
98 level_shift: 0.0,
99 damping: 0.0,
100 use_parallel_eri: false,
101 rohf: false,
102 }
103 }
104}
105
106fn build_spin_density(coefficients: &DMatrix<f64>, n_occ: usize) -> DMatrix<f64> {
108 let n = coefficients.nrows();
109 let mut p = DMatrix::zeros(n, n);
110 for i in 0..n {
111 for j in 0..=i {
112 let mut val = 0.0;
113 for k in 0..n_occ {
114 val += coefficients[(i, k)] * coefficients[(j, k)];
115 }
116 p[(i, j)] = val;
117 p[(j, i)] = val;
118 }
119 }
120 p
121}
122
123fn build_uhf_fock(
127 h_core: &DMatrix<f64>,
128 p_total: &DMatrix<f64>,
129 p_spin: &DMatrix<f64>,
130 eris: &TwoElectronIntegrals,
131) -> DMatrix<f64> {
132 let n = h_core.nrows();
133 let mut fock = h_core.clone();
134
135 for mu in 0..n {
136 for nu in 0..=mu {
137 let mut g = 0.0;
138 for lam in 0..n {
139 for sig in 0..n {
140 let j = p_total[(lam, sig)] * eris.get(mu, nu, lam, sig);
142 let k = p_spin[(lam, sig)] * eris.get(mu, lam, nu, sig);
144 g += j - k;
145 }
146 }
147 fock[(mu, nu)] += g;
148 if mu != nu {
149 fock[(nu, mu)] += g;
150 }
151 }
152 }
153 fock
154}
155
156fn uhf_electronic_energy(
160 p_alpha: &DMatrix<f64>,
161 p_beta: &DMatrix<f64>,
162 h_core: &DMatrix<f64>,
163 f_alpha: &DMatrix<f64>,
164 f_beta: &DMatrix<f64>,
165) -> f64 {
166 let n = h_core.nrows();
167 let mut e = 0.0;
168 for i in 0..n {
169 for j in 0..n {
170 let p_t = p_alpha[(i, j)] + p_beta[(i, j)];
171 e += p_t * h_core[(i, j)]
172 + p_alpha[(i, j)] * f_alpha[(i, j)]
173 + p_beta[(i, j)] * f_beta[(i, j)];
174 }
175 }
176 0.5 * e
177}
178
179fn compute_s2(
183 c_alpha: &DMatrix<f64>,
184 c_beta: &DMatrix<f64>,
185 overlap: &DMatrix<f64>,
186 n_alpha: usize,
187 n_beta: usize,
188) -> f64 {
189 let sz = 0.5 * (n_alpha as f64 - n_beta as f64);
190 let mut overlap_sum = 0.0;
191
192 for i in 0..n_alpha {
194 for j in 0..n_beta {
195 let mut s_ij = 0.0;
196 let n = overlap.nrows();
197 for mu in 0..n {
198 for nu in 0..n {
199 s_ij += c_alpha[(mu, i)] * overlap[(mu, nu)] * c_beta[(nu, j)];
200 }
201 }
202 overlap_sum += s_ij * s_ij;
203 }
204 }
205
206 sz * (sz + 1.0) + n_beta as f64 - overlap_sum
207}
208
209fn density_rms(a: &DMatrix<f64>, b: &DMatrix<f64>) -> f64 {
211 let n = a.nrows();
212 let mut sum = 0.0;
213 for i in 0..n {
214 for j in 0..n {
215 let d = a[(i, j)] - b[(i, j)];
216 sum += d * d;
217 }
218 }
219 (sum / (n * n) as f64).sqrt()
220}
221
222fn diag_fock(fock: &DMatrix<f64>, x: &DMatrix<f64>, n_basis: usize) -> (DMatrix<f64>, Vec<f64>) {
225 let f_ortho = transform_to_orthogonal(fock, x);
226 let eigen = f_ortho.symmetric_eigen();
227
228 let mut indices: Vec<usize> = (0..n_basis).collect();
229 indices.sort_by(|&a, &b| {
230 eigen.eigenvalues[a]
231 .partial_cmp(&eigen.eigenvalues[b])
232 .unwrap()
233 });
234
235 let mut c_ortho = DMatrix::zeros(n_basis, n_basis);
236 let mut energies = vec![0.0; n_basis];
237 for (new_idx, &old_idx) in indices.iter().enumerate() {
238 energies[new_idx] = eigen.eigenvalues[old_idx];
239 for i in 0..n_basis {
240 c_ortho[(i, new_idx)] = eigen.eigenvectors[(i, old_idx)];
241 }
242 }
243
244 let c = back_transform(&c_ortho, x);
245 (c, energies)
246}
247
248pub fn run_uhf(system: &MolecularSystem, config: &UhfConfig) -> UhfResult {
255 let n_electrons = system.n_electrons();
256 let n_unpaired = (system.multiplicity as usize).saturating_sub(1);
257 let n_alpha = (n_electrons + n_unpaired) / 2;
258 let n_beta = n_electrons - n_alpha;
259
260 let basis = BasisSet::sto3g(&system.atomic_numbers, &system.positions_bohr);
262 let n_basis = basis.n_basis;
263
264 let core = CoreMatrices::build(&basis, &system.atomic_numbers, &system.positions_bohr);
266 let s = &core.overlap;
267 let h_core = &core.core_hamiltonian;
268 let e_nuc = nuclear_repulsion_energy(&system.atomic_numbers, &system.positions_bohr);
269
270 let eris = {
272 #[cfg(feature = "parallel")]
273 {
274 if config.use_parallel_eri {
275 TwoElectronIntegrals::compute_parallel(&basis)
276 } else {
277 TwoElectronIntegrals::compute(&basis)
278 }
279 }
280 #[cfg(not(feature = "parallel"))]
281 {
282 TwoElectronIntegrals::compute(&basis)
283 }
284 };
285
286 let (x, _) = lowdin_orthogonalization(s, 1e-8);
288
289 let (mut c_alpha, mut e_alpha) = diag_fock(h_core, &x, n_basis);
291 let (mut c_beta, mut e_beta) = diag_fock(h_core, &x, n_basis);
292
293 let mut p_alpha = build_spin_density(&c_alpha, n_alpha);
294 let mut p_beta = build_spin_density(&c_beta, n_beta);
295 let mut p_total = &p_alpha + &p_beta;
296
297 let mut p_alpha_old = p_alpha.clone();
298 let mut p_beta_old = p_beta.clone();
299
300 let mut diis_alpha = DiisAccelerator::new(config.diis_size);
302 let mut diis_beta = DiisAccelerator::new(config.diis_size);
303 let mut e_total = 0.0;
304 let mut converged = false;
305 let mut scf_iter = 0;
306 let mut f_alpha = h_core.clone();
307 let mut f_beta = h_core.clone();
308
309 for iteration in 0..config.max_iterations {
310 scf_iter = iteration + 1;
311
312 f_alpha = build_uhf_fock(h_core, &p_total, &p_alpha, &eris);
314 f_beta = build_uhf_fock(h_core, &p_total, &p_beta, &eris);
315
316 if config.level_shift > 0.0 {
318 for i in 0..n_basis {
319 f_alpha[(i, i)] += config.level_shift;
320 f_beta[(i, i)] += config.level_shift;
321 }
322 }
323
324 if config.diis_size > 0 {
326 diis_alpha.add_iteration(&f_alpha, &p_alpha, s);
327 diis_beta.add_iteration(&f_beta, &p_beta, s);
328 if let Some(fa) = diis_alpha.extrapolate() {
329 f_alpha = fa;
330 }
331 if let Some(fb) = diis_beta.extrapolate() {
332 f_beta = fb;
333 }
334 }
335
336 let (ca_new, ea_new) = diag_fock(&f_alpha, &x, n_basis);
338 let (cb_new, eb_new) = diag_fock(&f_beta, &x, n_basis);
339 c_alpha = ca_new;
340 c_beta = cb_new;
341 e_alpha = ea_new;
342 e_beta = eb_new;
343
344 let pa_new = build_spin_density(&c_alpha, n_alpha);
345 let pb_new = build_spin_density(&c_beta, n_beta);
346
347 if config.damping > 0.0 {
348 p_alpha = &pa_new * (1.0 - config.damping) + &p_alpha_old * config.damping;
349 p_beta = &pb_new * (1.0 - config.damping) + &p_beta_old * config.damping;
350 } else {
351 p_alpha = pa_new;
352 p_beta = pb_new;
353 }
354 p_total = &p_alpha + &p_beta;
355
356 let e_elec = uhf_electronic_energy(&p_alpha, &p_beta, h_core, &f_alpha, &f_beta);
357 let e_new = e_elec + e_nuc;
358
359 let delta_e = (e_new - e_total).abs();
360 let delta_pa = density_rms(&p_alpha, &p_alpha_old);
361 let delta_pb = density_rms(&p_beta, &p_beta_old);
362 let delta_p = delta_pa.max(delta_pb);
363
364 if delta_e < config.energy_threshold && delta_p < config.density_threshold && iteration > 0
365 {
366 converged = true;
367 e_total = e_new;
368 break;
369 }
370
371 e_total = e_new;
372 p_alpha_old = p_alpha.clone();
373 p_beta_old = p_beta.clone();
374 }
375
376 if config.rohf {
378 let f_eff = (&f_alpha + &f_beta) * 0.5;
380 let (c_rohf, e_rohf) = diag_fock(&f_eff, &x, n_basis);
381 p_alpha = build_spin_density(&c_rohf, n_alpha);
383 p_beta = build_spin_density(&c_rohf, n_beta);
384 p_total = &p_alpha + &p_beta;
385 c_alpha = c_rohf.clone();
386 c_beta = c_rohf;
387 e_alpha = e_rohf.clone();
388 e_beta = e_rohf;
389 }
390
391 let e_elec = uhf_electronic_energy(&p_alpha, &p_beta, h_core, &f_alpha, &f_beta);
392
393 let homo_a = if n_alpha > 0 {
395 e_alpha[n_alpha - 1]
396 } else {
397 f64::NEG_INFINITY
398 };
399 let homo_b = if n_beta > 0 {
400 e_beta[n_beta - 1]
401 } else {
402 f64::NEG_INFINITY
403 };
404 let homo = homo_a.max(homo_b);
405
406 let lumo_a = if n_alpha < n_basis {
407 Some(e_alpha[n_alpha])
408 } else {
409 None
410 };
411 let lumo_b = if n_beta < n_basis {
412 Some(e_beta[n_beta])
413 } else {
414 None
415 };
416 let lumo = match (lumo_a, lumo_b) {
417 (Some(a), Some(b)) => Some(a.min(b)),
418 (Some(a), None) => Some(a),
419 (None, Some(b)) => Some(b),
420 _ => None,
421 };
422 let gap = lumo.map(|l| (l - homo) * HARTREE_TO_EV).unwrap_or(0.0);
423
424 let s2 = compute_s2(&c_alpha, &c_beta, s, n_alpha, n_beta);
426 let s_exact = 0.5 * n_unpaired as f64;
427 let s2_exact = s_exact * (s_exact + 1.0);
428 let contamination = s2 - s2_exact;
429
430 let mulliken = super::mulliken::mulliken_analysis(
432 &p_total,
433 s,
434 &basis.function_to_atom,
435 &system.atomic_numbers,
436 );
437
438 UhfResult {
439 alpha_orbital_energies: e_alpha,
440 beta_orbital_energies: e_beta,
441 alpha_coefficients: c_alpha,
442 beta_coefficients: c_beta,
443 alpha_density: p_alpha,
444 beta_density: p_beta,
445 total_density: p_total,
446 electronic_energy: e_elec,
447 nuclear_repulsion: e_nuc,
448 total_energy: e_total,
449 homo_energy: homo,
450 lumo_energy: lumo,
451 gap_ev: gap,
452 mulliken_charges: mulliken.charges,
453 scf_iterations: scf_iter,
454 converged,
455 n_basis,
456 n_alpha,
457 n_beta,
458 s2_expectation: s2,
459 spin_contamination: contamination,
460 overlap_matrix: s.clone(),
461 alpha_fock: f_alpha,
462 beta_fock: f_beta,
463 }
464}
465
466pub fn run_uhf_parallel(system: &MolecularSystem) -> UhfResult {
468 run_uhf(
469 system,
470 &UhfConfig {
471 use_parallel_eri: true,
472 ..UhfConfig::default()
473 },
474 )
475}
476
477pub fn run_rohf(system: &MolecularSystem) -> UhfResult {
479 run_uhf(
480 system,
481 &UhfConfig {
482 rohf: true,
483 use_parallel_eri: true,
484 ..UhfConfig::default()
485 },
486 )
487}