use crate::fock::build_fock;
use crate::hamiltonian::build_hcore;
use crate::integrals::core_repulsion::compute_total_core_repulsion;
use crate::parameters::ParameterModel;
use crate::scf::density::{compute_density_matrix, compute_electronic_energy, max_density_diff};
use crate::scf::eigensolver::diagonalize_symmetric;
use crate::types::{MolecularBatch, ScfWorkspace};
#[derive(Debug, Clone, PartialEq)]
pub struct ScfResult {
pub converged: bool,
pub iterations: usize,
pub total_energy_ev: f64,
pub electronic_energy_ev: f64,
pub nuclear_repulsion_ev: f64,
pub homo_energy_ev: f64,
pub lumo_energy_ev: f64,
pub dielectric_energy_ev: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScfOptions {
pub max_iter: usize,
pub energy_tol_ev: f64,
pub density_tol: f64,
pub level_shift_ev: f64,
pub damping: f64,
pub use_nddo: bool,
pub cosmo: Option<crate::solvation::CosmoParams>,
pub electric_field_ev_angstrom: Option<[f64; 3]>,
}
impl Default for ScfOptions {
fn default() -> Self {
Self {
max_iter: 60,
energy_tol_ev: 1e-7,
density_tol: 1e-6,
level_shift_ev: 0.0,
damping: 0.5,
use_nddo: false,
cosmo: None,
electric_field_ev_angstrom: None,
}
}
}
pub fn apply_level_shift(
fock: &mut crate::types::AlignedMatrix<f64>,
density: &crate::types::AlignedMatrix<f64>,
shift_ev: f64,
) {
if shift_ev.abs() < 1e-12 {
return;
}
let norbs = fock.rows;
for i in 0..norbs {
for j in 0..norbs {
let f_val = fock.get(i, j);
let p_val = density.get(i, j);
let shift_term = if i == j {
shift_ev - 0.5 * shift_ev * p_val
} else {
-0.5 * shift_ev * p_val
};
fock.set(i, j, f_val + shift_term);
}
}
}
pub fn run_rhf_scf_with_options(
batch: &MolecularBatch,
model: &dyn ParameterModel,
ws: &mut ScfWorkspace,
options: &ScfOptions,
) -> ScfResult {
assert_eq!(ws.norbs, batch.norbs);
let mut total_valence_elecs = 0.0;
for &z in &batch.atomic_numbers {
if let Some(p) = model.get_element(z) {
total_valence_elecs += p.core_charge;
}
}
let nocc = (total_valence_elecs.round() as usize) / 2;
assert!(
nocc > 0,
"System must have at least one electron pair for closed-shell RHF"
);
let mut enuc = compute_total_core_repulsion(batch, model);
let mut cosmo_state = if let Some(cosmo_params) = options.cosmo {
crate::solvation::CosmoState::initialize(batch, model, cosmo_params).ok()
} else {
None
};
if let Some(ref cs) = cosmo_state {
enuc += cs.e_nuc_diel_ev;
}
if options.use_nddo {
ws.diatomic_pairs = crate::integrals::multipoles::precompute_diatomic_pairs(batch, model);
crate::hamiltonian::hcore::build_hcore_nddo(
batch,
model,
&ws.diatomic_pairs,
&mut ws.h_core,
);
} else {
build_hcore(batch, model, &mut ws.h_core);
}
if let Some(ref cs) = cosmo_state {
cs.apply_nuclear_reaction_field_to_hcore(&mut ws.h_core);
}
if let Some(efield) = options.electric_field_ev_angstrom {
let e_nuc_field = crate::hamiltonian::hcore::apply_electric_field_to_hcore(
batch,
model,
&mut ws.h_core,
efield,
);
enuc += e_nuc_field;
}
diagonalize_symmetric(&ws.h_core, &mut ws.eigenvalues, &mut ws.eigenvectors);
compute_density_matrix(&ws.eigenvectors, nocc, &mut ws.density);
ws.diis.reset();
let mut prev_energy = 0.0f64;
let mut converged = false;
let mut iters_done = 0;
let mut last_diel_ev: Option<f64> = None;
let mut current_eff_shift = 0.0f64;
for iter in 1..=options.max_iter {
iters_done = iter;
if options.use_nddo {
crate::fock::fock_builder::build_fock_nddo(
batch,
model,
&ws.diatomic_pairs,
&ws.h_core,
&ws.density,
&mut ws.fock,
);
} else {
build_fock(batch, model, &ws.h_core, &ws.density, &mut ws.fock);
}
if let Some(ref mut cs) = cosmo_state {
let ediel = cs.apply_electronic_reaction_field_to_fock(&ws.density, &mut ws.fock);
last_diel_ev = Some(ediel);
}
let e_elec = compute_electronic_energy(&ws.density, &ws.h_core, &ws.fock);
let e_total = e_elec + enuc;
let diis_res = ws
.diis
.push_and_extrapolate(&mut ws.fock, &ws.density, &mut ws.tmp2);
let eff_shift = if options.level_shift_ev > 0.0 && iter > 2 {
options.level_shift_ev
} else if options.level_shift_ev == 0.0 && iter > 25 {
2.0 } else {
0.0
};
current_eff_shift = eff_shift;
if eff_shift > 0.0 {
apply_level_shift(&mut ws.fock, &ws.density, eff_shift);
}
diagonalize_symmetric(&ws.fock, &mut ws.eigenvalues, &mut ws.eigenvectors);
compute_density_matrix(&ws.eigenvectors, nocc, &mut ws.tmp1);
let delta_e = (e_total - prev_energy).abs();
let delta_p = max_density_diff(&ws.tmp1, &ws.density);
if iter > 1
&& delta_e < options.energy_tol_ev
&& (delta_p < options.density_tol || diis_res.max_error < options.density_tol)
{
converged = true;
ws.density.data.copy_from_slice(&ws.tmp1.data);
break;
}
prev_energy = e_total;
if diis_res.extrapolated {
ws.density.data.copy_from_slice(&ws.tmp1.data);
} else {
let d = if iter > 25 && (options.damping - 0.5).abs() < 1e-4 {
0.3 } else {
options.damping
};
for i in 0..ws.norbs {
for j in 0..ws.norbs {
let p_new = ws.tmp1.get(i, j);
let p_old = ws.density.get(i, j);
ws.density.set(i, j, (1.0 - d) * p_new + d * p_old);
}
}
}
}
let homo = ws.eigenvalues[nocc - 1];
let lumo = if nocc < ws.norbs {
if current_eff_shift > 0.0 {
ws.eigenvalues[nocc] - current_eff_shift
} else {
ws.eigenvalues[nocc]
}
} else {
0.0
};
let e_elec_final = compute_electronic_energy(&ws.density, &ws.h_core, &ws.fock);
ScfResult {
converged,
iterations: iters_done,
total_energy_ev: e_elec_final + enuc,
electronic_energy_ev: e_elec_final,
nuclear_repulsion_ev: enuc,
homo_energy_ev: homo,
lumo_energy_ev: lumo,
dielectric_energy_ev: last_diel_ev,
}
}
pub fn run_rhf_scf(
batch: &MolecularBatch,
model: &dyn ParameterModel,
ws: &mut ScfWorkspace,
max_iter: usize,
energy_tol_ev: f64,
density_tol: f64,
) -> ScfResult {
run_rhf_scf_with_options(
batch,
model,
ws,
&ScfOptions {
max_iter,
energy_tol_ev,
density_tol,
level_shift_ev: 0.0,
damping: 0.5,
use_nddo: false,
cosmo: None,
electric_field_ev_angstrom: None,
},
)
}
pub fn run_rhf_scf_adaptive(
batch: &MolecularBatch,
model: &dyn ParameterModel,
ws: &mut ScfWorkspace,
max_iter_per_stage: usize,
energy_tol_ev: f64,
density_tol: f64,
) -> ScfResult {
run_rhf_scf_adaptive_with_nddo(
batch,
model,
ws,
max_iter_per_stage,
energy_tol_ev,
density_tol,
false,
)
}
pub fn run_rhf_scf_adaptive_with_nddo(
batch: &MolecularBatch,
model: &dyn ParameterModel,
ws: &mut ScfWorkspace,
max_iter_per_stage: usize,
energy_tol_ev: f64,
density_tol: f64,
use_nddo: bool,
) -> ScfResult {
run_rhf_scf_adaptive_with_nddo_and_cosmo(
batch,
model,
ws,
max_iter_per_stage,
energy_tol_ev,
density_tol,
use_nddo,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn run_rhf_scf_adaptive_with_nddo_and_cosmo(
batch: &MolecularBatch,
model: &dyn ParameterModel,
ws: &mut ScfWorkspace,
max_iter_per_stage: usize,
energy_tol_ev: f64,
density_tol: f64,
use_nddo: bool,
cosmo: Option<crate::solvation::CosmoParams>,
) -> ScfResult {
let stages = [
ScfOptions {
max_iter: max_iter_per_stage,
energy_tol_ev,
density_tol,
level_shift_ev: 0.0,
damping: 0.5,
use_nddo,
cosmo,
electric_field_ev_angstrom: None,
},
ScfOptions {
max_iter: max_iter_per_stage * 2,
energy_tol_ev,
density_tol,
level_shift_ev: 8.0,
damping: 0.5,
use_nddo,
cosmo,
electric_field_ev_angstrom: None,
},
ScfOptions {
max_iter: max_iter_per_stage * 2,
energy_tol_ev,
density_tol,
level_shift_ev: 4.44,
damping: 0.7,
use_nddo,
cosmo,
electric_field_ev_angstrom: None,
},
ScfOptions {
max_iter: max_iter_per_stage * 2,
energy_tol_ev,
density_tol,
level_shift_ev: 8.0,
damping: 0.7,
use_nddo,
cosmo,
electric_field_ev_angstrom: None,
},
];
let mut last_res = ScfResult {
converged: false,
iterations: 0,
total_energy_ev: 0.0,
electronic_energy_ev: 0.0,
nuclear_repulsion_ev: 0.0,
homo_energy_ev: 0.0,
lumo_energy_ev: 0.0,
dielectric_energy_ev: None,
};
for (stage_idx, opts) in stages.iter().enumerate() {
if stage_idx > 0 {
ws.reset();
}
let res = run_rhf_scf_with_options(batch, model, ws, opts);
last_res = res;
if last_res.converged {
break;
}
}
last_res
}
#[allow(clippy::too_many_arguments)]
pub fn run_rhf_scf_adaptive_with_field(
batch: &MolecularBatch,
model: &dyn ParameterModel,
ws: &mut ScfWorkspace,
max_iter_per_stage: usize,
energy_tol_ev: f64,
density_tol: f64,
use_nddo: bool,
efield_ev_angstrom: [f64; 3],
) -> ScfResult {
let stages = [
ScfOptions {
max_iter: max_iter_per_stage,
energy_tol_ev,
density_tol,
level_shift_ev: 0.0,
damping: 0.5,
use_nddo,
cosmo: None,
electric_field_ev_angstrom: Some(efield_ev_angstrom),
},
ScfOptions {
max_iter: max_iter_per_stage * 2,
energy_tol_ev,
density_tol,
level_shift_ev: 8.0,
damping: 0.5,
use_nddo,
cosmo: None,
electric_field_ev_angstrom: Some(efield_ev_angstrom),
},
ScfOptions {
max_iter: max_iter_per_stage * 2,
energy_tol_ev,
density_tol,
level_shift_ev: 4.44,
damping: 0.7,
use_nddo,
cosmo: None,
electric_field_ev_angstrom: Some(efield_ev_angstrom),
},
ScfOptions {
max_iter: max_iter_per_stage * 2,
energy_tol_ev,
density_tol,
level_shift_ev: 8.0,
damping: 0.7,
use_nddo,
cosmo: None,
electric_field_ev_angstrom: Some(efield_ev_angstrom),
},
];
let mut last_res = ScfResult {
converged: false,
iterations: 0,
total_energy_ev: 0.0,
electronic_energy_ev: 0.0,
nuclear_repulsion_ev: 0.0,
homo_energy_ev: 0.0,
lumo_energy_ev: 0.0,
dielectric_energy_ev: None,
};
for (stage_idx, opts) in stages.iter().enumerate() {
if stage_idx > 0 {
ws.reset();
}
let res = run_rhf_scf_with_options(batch, model, ws, opts);
last_res = res;
if last_res.converged {
break;
}
}
last_res
}