Skip to main content

sci_form/hf/
api.rs

1//! Public API for HF-3c composite method.
2//!
3//! Ties together: SCF (Hartree-Fock) + D3 + gCP + SRB corrections,
4//! plus optional CIS excited-state calculation for UV-Vis spectroscopy.
5
6use super::basis::{build_sto3g_basis, ANG_TO_BOHR};
7use super::cis::{compute_cis, CisResult};
8use super::d3::compute_d3_energy;
9use super::fock::nuclear_repulsion;
10use super::gcp::compute_gcp;
11use super::integrals::compute_eris;
12use super::nuclear::compute_nuclear_matrix;
13use super::overlap_kin::{compute_kinetic_matrix, compute_overlap_matrix};
14use super::scf::{solve_scf, ScfConfig};
15use super::srb::compute_srb;
16use serde::{Deserialize, Serialize};
17
18/// Configuration for HF-3c calculation.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct HfConfig {
21    /// Maximum SCF iterations.
22    pub max_scf_iter: usize,
23    /// DIIS subspace size.
24    pub diis_size: usize,
25    /// Number of CIS excited states to compute (0 = skip CIS).
26    pub n_cis_states: usize,
27    /// Include empirical corrections (D3, gCP, SRB).
28    pub corrections: bool,
29}
30
31impl Default for HfConfig {
32    fn default() -> Self {
33        HfConfig {
34            max_scf_iter: 100,
35            diis_size: 6,
36            n_cis_states: 5,
37            corrections: true,
38        }
39    }
40}
41
42/// Result of an HF-3c calculation.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Hf3cResult {
45    /// Total HF-3c energy (Hartree).
46    pub energy: f64,
47    /// Pure HF electronic energy.
48    pub hf_energy: f64,
49    /// Nuclear repulsion energy.
50    pub nuclear_repulsion: f64,
51    /// D3 dispersion correction energy.
52    pub d3_energy: f64,
53    /// gCP BSSE correction energy.
54    pub gcp_energy: f64,
55    /// SRB short-range correction energy.
56    pub srb_energy: f64,
57    /// Orbital energies (sorted).
58    pub orbital_energies: Vec<f64>,
59    /// Number of SCF iterations.
60    pub scf_iterations: usize,
61    /// Whether SCF converged.
62    pub converged: bool,
63    /// CIS excitation results (if requested).
64    pub cis: Option<CisResult>,
65}
66
67/// Run a complete HF-3c calculation.
68pub fn solve_hf3c(
69    elements: &[u8],
70    positions: &[[f64; 3]],
71    config: &HfConfig,
72) -> Result<Hf3cResult, String> {
73    if elements.len() != positions.len() {
74        return Err("elements/positions length mismatch".to_string());
75    }
76    if elements.is_empty() {
77        return Err("empty molecule".to_string());
78    }
79
80    // Convert positions to Bohr
81    let pos_bohr: Vec<[f64; 3]> = positions
82        .iter()
83        .map(|p| [p[0] * ANG_TO_BOHR, p[1] * ANG_TO_BOHR, p[2] * ANG_TO_BOHR])
84        .collect();
85
86    // Build basis set
87    let basis = build_sto3g_basis(elements, positions);
88    let n_basis = basis.n_basis();
89
90    // Count electrons
91    let n_electrons: usize = elements.iter().map(|&z| z as usize).sum();
92
93    // One-electron integrals
94    let s_mat = compute_overlap_matrix(&basis);
95    let t_mat = compute_kinetic_matrix(&basis);
96    let v_mat = compute_nuclear_matrix(&basis, elements, &pos_bohr);
97    let h_core = &t_mat + &v_mat;
98
99    // Two-electron integrals
100    let eris = compute_eris(&basis);
101
102    // SCF
103    let scf_config = ScfConfig {
104        max_iter: config.max_scf_iter,
105        diis_size: config.diis_size,
106        ..ScfConfig::default()
107    };
108    let scf_result = solve_scf(&h_core, &s_mat, &eris, n_electrons, &scf_config);
109
110    // Nuclear repulsion
111    let e_nuc = nuclear_repulsion(elements, &pos_bohr);
112
113    // Empirical corrections
114    let (d3_e, gcp_e, srb_e) = if config.corrections {
115        (
116            compute_d3_energy(elements, &pos_bohr).energy,
117            compute_gcp(elements, &pos_bohr),
118            compute_srb(elements, &pos_bohr),
119        )
120    } else {
121        (0.0, 0.0, 0.0)
122    };
123
124    let total = scf_result.energy + e_nuc + d3_e + gcp_e + srb_e;
125
126    // CIS excited states
127    let cis = if config.n_cis_states > 0 && scf_result.converged {
128        let n_occ = n_electrons / 2;
129        Some(compute_cis(
130            &scf_result.orbital_energies,
131            &scf_result.coefficients,
132            &eris,
133            n_basis,
134            n_occ,
135            config.n_cis_states,
136        ))
137    } else {
138        None
139    };
140
141    Ok(Hf3cResult {
142        energy: total,
143        hf_energy: scf_result.energy + e_nuc,
144        nuclear_repulsion: e_nuc,
145        d3_energy: d3_e,
146        gcp_energy: gcp_e,
147        srb_energy: srb_e,
148        orbital_energies: scf_result.orbital_energies,
149        scf_iterations: scf_result.iterations,
150        converged: scf_result.converged,
151        cis,
152    })
153}
154
155/// Batch-process multiple HF-3c calculations in parallel.
156#[cfg(feature = "parallel")]
157pub fn solve_hf3c_batch(
158    molecules: &[(&[u8], &[[f64; 3]])],
159    config: &HfConfig,
160) -> Vec<Result<Hf3cResult, String>> {
161    use rayon::prelude::*;
162    molecules
163        .par_iter()
164        .map(|(els, pos)| solve_hf3c(els, pos, config))
165        .collect()
166}
167
168/// Batch-process multiple HF-3c calculations sequentially.
169#[cfg(not(feature = "parallel"))]
170pub fn solve_hf3c_batch(
171    molecules: &[(&[u8], &[[f64; 3]])],
172    config: &HfConfig,
173) -> Vec<Result<Hf3cResult, String>> {
174    molecules
175        .iter()
176        .map(|(els, pos)| solve_hf3c(els, pos, config))
177        .collect()
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_h2_hf3c() {
186        let elements = [1u8, 1];
187        let positions = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.74]];
188        let config = HfConfig {
189            n_cis_states: 0,
190            ..Default::default()
191        };
192        let result = solve_hf3c(&elements, &positions, &config).unwrap();
193        assert!(result.energy.is_finite(), "Energy should be finite");
194        assert!(result.energy < 0.0, "H2 total energy should be negative");
195    }
196
197    #[test]
198    fn test_water_hf3c() {
199        let elements = [8u8, 1, 1];
200        let positions = [
201            [0.0, 0.0, 0.117],
202            [0.0, 0.757, -0.469],
203            [0.0, -0.757, -0.469],
204        ];
205        let result = solve_hf3c(&elements, &positions, &HfConfig::default()).unwrap();
206        assert!(result.energy.is_finite());
207        assert!(result.orbital_energies.len() == 7); // 7 basis functions
208    }
209}