1use super::integrals::get_eri;
11use nalgebra::{DMatrix, SymmetricEigen};
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Excitation {
17 pub energy: f64,
19 pub energy_ev: f64,
21 pub wavelength_nm: f64,
23 pub oscillator_strength: f64,
25 pub dominant_transition: (usize, usize),
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CisResult {
32 pub excitations: Vec<Excitation>,
34}
35
36const HARTREE_TO_EV: f64 = 27.21138602;
38
39pub fn compute_cis(
45 orbital_energies: &[f64],
46 coefficients: &DMatrix<f64>,
47 eris: &[f64],
48 n_basis: usize,
49 n_occupied: usize,
50 n_states: usize,
51) -> CisResult {
52 compute_cis_with_dipole(
53 orbital_energies,
54 coefficients,
55 eris,
56 n_basis,
57 n_occupied,
58 n_states,
59 None,
60 None,
61 )
62}
63
64pub fn compute_cis_with_dipole(
66 orbital_energies: &[f64],
67 coefficients: &DMatrix<f64>,
68 eris: &[f64],
69 n_basis: usize,
70 n_occupied: usize,
71 n_states: usize,
72 positions_bohr: Option<&[[f64; 3]]>,
73 basis_to_atom: Option<&[usize]>,
74) -> CisResult {
75 let n_virtual = n_basis - n_occupied;
76 let n_singles = n_occupied * n_virtual;
77
78 if n_singles == 0 {
79 return CisResult {
80 excitations: Vec::new(),
81 };
82 }
83
84 let mut h_cis = DMatrix::zeros(n_singles, n_singles);
86
87 for ia in 0..n_singles {
88 let i = ia / n_virtual;
89 let a = ia % n_virtual + n_occupied;
90
91 for jb in 0..=ia {
92 let j = jb / n_virtual;
93 let b = jb % n_virtual + n_occupied;
94
95 let mut val = 0.0;
96
97 if ia == jb {
99 val += orbital_energies[a] - orbital_energies[i];
100 }
101
102 let coulomb = mo_eri(coefficients, eris, n_basis, i, a, j, b);
104 let exchange = mo_eri(coefficients, eris, n_basis, i, j, a, b);
105
106 val += coulomb - exchange;
107
108 h_cis[(ia, jb)] = val;
109 h_cis[(jb, ia)] = val;
110 }
111 }
112
113 let eigen = SymmetricEigen::new(h_cis);
115
116 let mut indices: Vec<usize> = (0..n_singles).collect();
118 indices.sort_by(|&a, &b| {
119 eigen.eigenvalues[a]
120 .partial_cmp(&eigen.eigenvalues[b])
121 .unwrap()
122 });
123
124 let n_out = n_states.min(n_singles);
125 let mut excitations = Vec::with_capacity(n_out);
126
127 for &idx in indices.iter().take(n_out) {
128 let energy = eigen.eigenvalues[idx];
129 if energy <= 0.0 {
130 continue;
131 }
132
133 let energy_ev = energy * HARTREE_TO_EV;
134 let wavelength_nm = 1239.84193 / energy_ev;
135
136 let col = eigen.eigenvectors.column(idx);
138 let (dom_idx, _) = col
139 .iter()
140 .enumerate()
141 .max_by(|(_, a), (_, b)| a.abs().partial_cmp(&b.abs()).unwrap())
142 .unwrap();
143 let dom_i = dom_idx / n_virtual;
144 let dom_a = dom_idx % n_virtual + n_occupied;
145
146 let f_osc = match (positions_bohr, basis_to_atom) {
148 (Some(pos), Some(b2a)) => {
149 let mut tdm = [0.0f64; 3];
151 for single in 0..n_singles {
152 let i_s = single / n_virtual;
153 let a_s = single % n_virtual + n_occupied;
154 let x_ia = col[single];
155 if x_ia.abs() < 1e-12 {
156 continue;
157 }
158 let n_atoms = pos.len();
160 let mut q_atom = vec![0.0f64; n_atoms];
161 for mu in 0..n_basis {
162 let atom = b2a[mu];
163 let mut s_contrib = 0.0;
164 for nu in 0..n_basis {
165 s_contrib += coefficients[(nu, a_s)] * if mu == nu { 1.0 } else { 0.0 };
166 }
167 q_atom[atom] += coefficients[(mu, i_s)] * s_contrib;
168 }
169 for atom in 0..n_atoms {
170 tdm[0] += x_ia * q_atom[atom] * pos[atom][0];
171 tdm[1] += x_ia * q_atom[atom] * pos[atom][1];
172 tdm[2] += x_ia * q_atom[atom] * pos[atom][2];
173 }
174 }
175 let sqrt2 = std::f64::consts::SQRT_2;
176 tdm[0] *= sqrt2;
177 tdm[1] *= sqrt2;
178 tdm[2] *= sqrt2;
179 let tdm_sq = tdm[0] * tdm[0] + tdm[1] * tdm[1] + tdm[2] * tdm[2];
180 2.0 / 3.0 * energy * tdm_sq
181 }
182 _ => {
183 2.0 / 3.0 * energy * transition_dipole_sq(col.as_slice(), n_occupied, n_virtual)
185 }
186 };
187
188 excitations.push(Excitation {
189 energy,
190 energy_ev,
191 wavelength_nm,
192 oscillator_strength: f_osc,
193 dominant_transition: (dom_i, dom_a),
194 });
195 }
196
197 CisResult { excitations }
198}
199
200fn mo_eri(c: &DMatrix<f64>, eris: &[f64], n: usize, p: usize, q: usize, r: usize, s: usize) -> f64 {
206 let mut half1 = vec![0.0f64; n * n * n]; for lam in 0..n {
209 for sig in 0..n {
210 let c_sig_s = c[(sig, s)];
211 if c_sig_s.abs() < 1e-12 {
212 continue;
213 }
214 for mu in 0..n {
215 for nu in 0..n {
216 half1[mu * n * n + nu * n + lam] +=
217 c_sig_s * get_eri(eris, mu, nu, lam, sig, n);
218 }
219 }
220 }
221 }
222
223 let mut half2 = vec![0.0f64; n * n]; for lam in 0..n {
226 let c_lam_r = c[(lam, r)];
227 if c_lam_r.abs() < 1e-12 {
228 continue;
229 }
230 for mu in 0..n {
231 for nu in 0..n {
232 half2[mu * n + nu] += c_lam_r * half1[mu * n * n + nu * n + lam];
233 }
234 }
235 }
236
237 let mut val = 0.0;
239 for mu in 0..n {
240 let c_mu_p = c[(mu, p)];
241 if c_mu_p.abs() < 1e-12 {
242 continue;
243 }
244 for nu in 0..n {
245 let c_nu_q = c[(nu, q)];
246 if c_nu_q.abs() < 1e-12 {
247 continue;
248 }
249 val += c_mu_p * c_nu_q * half2[mu * n + nu];
250 }
251 }
252 val
253}
254
255fn transition_dipole_sq(cis_vec: &[f64], _n_occ: usize, _n_virt: usize) -> f64 {
257 cis_vec.iter().map(|c| c * c).sum::<f64>()
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_cis_dimensions() {
267 let n_occ = 2;
268 let n_virt = 3;
269 let n_basis = n_occ + n_virt;
270 let n_singles = n_occ * n_virt;
271
272 let energies = vec![-2.0, -1.0, 0.5, 1.0, 1.5];
274 let coeffs = DMatrix::identity(n_basis, n_basis);
275 let eris = vec![0.0; n_basis * (n_basis + 1) / 2 * (n_basis * (n_basis + 1) / 2 + 1) / 2];
276
277 let result = compute_cis(&energies, &coeffs, &eris, n_basis, n_occ, 3);
278 assert!(
279 result.excitations.len() <= n_singles,
280 "CIS states ≤ n_singles"
281 );
282 }
283}